diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2ff6c7e6d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# Ignore everything +* + +# Only allow the following for docker build: +!backend/ +!docker/ +!scripts/ +!test/ diff --git a/.gitignore b/.gitignore index 08462849d..d3fde6d23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,29 @@ +git.idea +.env .DS_Store -.idea ._* +*.code-workspace +vendor +bin/* +backend/config.json +backend/embed/assets +backend/.task +backend/coverage.out +backend/coverage.html +test/node_modules +*/node_modules +docs/.vuepress/dist +frontend/build +frontend/yarn-error.log +frontend/.npmrc +frontend/src/locale/lang +test/cypress/fixtures/example.json .vscode -certbot-help.txt +docker-build +data +dist +backend/embed/acme.sh +docker/dev/resolv.conf +docker/dev/dnsrouter-config.json.tmp +thunder-tests +test/cypress/videos diff --git a/.version b/.version index b0e185b74..5efd7ac5b 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.9.18 +3.0.0a diff --git a/Jenkinsfile b/Jenkinsfile index 1b7446924..03f054d60 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,21 +1,36 @@ +import groovy.transform.Field + +@Field +def buildxPushTags = "" + +def getVersion() { + ver = sh(script: 'cat .version', returnStdout: true) + return ver.trim() +} + +def getCommit() { + ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true) + return ver.trim() +} + pipeline { agent { label 'docker-multiarch' } options { - buildDiscarder(logRotator(numToKeepStr: '5')) + buildDiscarder(logRotator(numToKeepStr: '10')) disableConcurrentBuilds() ansiColor('xterm') } environment { - IMAGE = "nginx-proxy-manager" + DOCKER_ORG = 'jc21' + IMAGE = 'nginx-proxy-manager' BUILD_VERSION = getVersion() - MAJOR_VERSION = "2" - BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" - COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" - COMPOSE_FILE = 'docker/docker-compose.ci.yml' + BUILD_COMMIT = getCommit() + MAJOR_VERSION = '3' + BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}" + BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_INTERACTIVE_NO_CLI = 1 - BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" } stages { stage('Environment') { @@ -26,7 +41,9 @@ pipeline { } steps { script { - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" + buildxPushTags = "-t docker.io/${DOCKER_ORG}/${IMAGE}:${BUILD_VERSION} -t docker.io/${DOCKER_ORG}/${IMAGE}:${MAJOR_VERSION} -t docker.io/${DOCKER_ORG}/${IMAGE}:latest" + echo 'Building on Master is disabled!' + sh 'exit 1' } } } @@ -39,121 +56,146 @@ pipeline { steps { script { // Defaults to the Branch name, which is applies to all branches AND pr's - env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" + // buildxPushTags = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}" + buildxPushTags = "-t docker.io/${DOCKER_ORG}/${IMAGE}:v3" + } + } + } + } + } + stage('Build') { + parallel { + stage('Project') { + steps { + sh './scripts/ci/build-frontend' + sh './scripts/ci/test-backend' + // Temporarily disable building backend binaries + // sh './scripts/ci/build-backend' + // Build the docker image used for testing below + sh '''docker build --pull --no-cache \\ + -t "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" \\ + -f docker/Dockerfile \\ + --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\ + --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\ + --build-arg BUILD_VERSION="${BUILD_VERSION}" \\ + . + ''' + } + post { + success { + junit 'test/results/junit/*' + // archiveArtifacts allowEmptyArchive: false, artifacts: 'bin/*' + publishHTML([ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: false, + reportDir: 'test/results/html-reports', + reportFiles: 'backend-coverage.html', + reportName: 'HTML Reports', + useWrapperFileDirectly: true + ]) } } } - stage('Versions') { + stage('Docs') { steps { - 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' + dir(path: 'docs') { + sh 'yarn install' + sh 'yarn build' + } } } } } - stage('Frontend') { - steps { - sh './scripts/frontend-build' + stage('Test Sqlite') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite" + COMPOSE_FILE = 'docker/docker-compose.ci.yml' } - } - stage('Backend') { - steps { - echo 'Checking Syntax ...' - sh 'docker pull nginxproxymanager/nginx-full:certbot-node' - // See: https://github.com/yarnpkg/yarn/issues/3254 - sh '''docker run --rm \\ - -v "$(pwd)/backend:/app" \\ - -v "$(pwd)/global:/app/global" \\ - -w /app \\ - nginxproxymanager/nginx-full:certbot-node \\ - 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')" \\ - . - ''' + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } } - } - 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/' + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + // Adding this here as the schema needs to come from a running stack, but this will be used by docs later + sh 'docker-compose exec -T fullstack curl -s --output /temp-docs/api-schema.json "http://fullstack:81/api/schema"' } 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' - } + sh 'mkdir -p debug/sqlite' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1' junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } } } - stage('Integration Tests Mysql') { + stage('Test Mysql') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml' + } + 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' - - // Run tests - sh 'rm -rf test/results' - sh 'docker-compose up cypress-mysql' - // Get results - sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/' + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' } 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' - // Cypress videos and screenshot artifacts - dir(path: 'test/results') { - archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' - } + sh 'mkdir -p debug/mysql' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1' junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } } } - stage('Docs') { + stage('Test Postgres') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml' + } when { not { equals expected: 'UNSTABLE', actual: currentBuild.result } } steps { - dir(path: 'docs') { - sh 'yarn install' - sh 'yarn build' - } + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + } + post { + always { + // Dumps to analyze later + sh 'mkdir -p debug/postgres' + sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1' + sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1' - dir(path: 'docs/.vuepress/dist') { - sh 'tar -czf ../../docs.tgz *' + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' } - - archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) } } stage('MultiArch Build') { @@ -164,90 +206,63 @@ pipeline { } 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}" + sh 'docker login -u "${duser}" -p "${dpass}"' + sh "./scripts/buildx --push ${buildxPushTags}" + // sh './scripts/buildx -o type=local,dest=docker-build' } } } - stage('Docs Deploy') { - when { - allOf { - branch 'master' - not { - equals expected: 'UNSTABLE', actual: currentBuild.result + stage('Docs / Comment') { + parallel { + stage('Docs Job') { + when { + allOf { + branch pattern: "^(develop|master|v3)\$", comparator: "REGEXP" + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } } - } - } - steps { - 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 AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ - -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ - -v \$(pwd):/app \\ - -w /app \\ - jc21/ci-tools \\ - scripts/docs-upload /app/docs/.vuepress/dist/ - """ - - sh """docker run --rm \\ - --name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\ - -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 '/*' - """ - } - } - } - stage('PR Comment') { - when { - allOf { - changeRequest() - not { - equals expected: 'UNSTABLE', actual: currentBuild.result + steps { + build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")] } } - } - 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.") + stage('PR Comment') { + when { + allOf { + changeRequest() + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + } + steps { + script { + npmGithubPrComment("Docker 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.", true) + } + } } } } } post { always { - sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' - sh 'echo Reverting ownership' - sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' - } - success { - juxtapose event: 'success' - sh 'figlet "SUCCESS"' + sh './scripts/ci/build-cleanup' + echo 'Reverting ownership' + sh 'docker run --rm -v $(pwd):/data jc21/gotools:latest chown -R "$(id -u):$(id -g)" /data' + printResult() } failure { - archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) - juxtapose event: 'failure' - sh 'figlet "FAILURE"' + archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true) + dir(path: 'test') { + archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml' + } } unstable { - archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true) - juxtapose event: 'unstable' - sh 'figlet "UNSTABLE"' + archiveArtifacts(artifacts: 'debug/**/*', allowEmptyArchive: true) + dir(path: 'test') { + archiveArtifacts allowEmptyArchive: true, artifacts: 'results/**/*', excludes: '**/*.xml' + } } } } - -def getVersion() { - ver = sh(script: 'cat .version', returnStdout: true) - return ver.trim() -} - -def getCommit() { - ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true) - return ver.trim() -} diff --git a/README.md b/README.md index a97d3ba87..a66ba16f7 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,13 @@



- + - - Gitter - - - Reddit -

This project comes as a pre-built docker image that enables you to easily forward to your websites @@ -35,7 +29,7 @@ so that the barrier for entry here is low. ## Features -- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/) +- Beautiful and Secure Admin Interface based on [Chakra UI](https://chakra-ui.com/) - Easily create forwarding domains, redirections, streams and 404 hosts without knowing anything about Nginx - Free SSL using Let's Encrypt or provide your own custom SSL certificates - Access Lists and basic HTTP Authentication for your hosts @@ -52,7 +46,8 @@ 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 + +## Quickest Setup 1. Install Docker and Docker-Compose @@ -65,7 +60,7 @@ I won't go in to too much detail here but here are the basics for someone new to version: '3' services: app: - image: 'jc21/nginx-proxy-manager:latest' + image: 'jc21/nginx-proxy-manager:v3' restart: unless-stopped ports: - '80:80' @@ -73,460 +68,34 @@ services: - '443:443' volumes: - ./data:/data - - ./letsencrypt:/etc/letsencrypt ``` 3. Bring up your stack by running ```bash docker-compose up -d + +# If using docker-compose-plugin +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 -``` +## Contributors -Immediately after logging in with this default user you will be asked to modify your details and change your password. +Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). +## Getting Support -## Contributors +1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) +2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) +3. [Development Gitter](https://gitter.im/nginx-proxy-manager/community) +4. [Reddit](https://reddit.com/r/nginxproxymanager) -Special thanks to the following contributors: +## Become a Contributor - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
chaptergy -
-
- - -
Kyle Klaus -
-
- - -
ƬHE ЯAW -
-
- - -
Spencer -
-
- - -
Xantios Krugor -
-
- - -
David Panesso -
-
- - -
IronTooch -
-
- - -
Damiano -
-
- - -
Russ -
-
- - -
Marcelo Castagna -
-
- - -
Steven Harris -
-
- - -
Jocelyn Le Sage -
-
- - -
Carl Mercier -
-
- - -
Paul Mansfield -
-
- - -
OhHeyAlan -
-
- - -
Carl Sutton -
-
- - -
Gergő Törcsvári -
-
- - -
vrenjith -
-
- - -
David Rivera -
-
- - -
Jaap-Jan de Wit -
-
- - -
James Morgan -
-
- - -
Sebastian Valle -
-
- - -
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 -
-
- - -
demize -
-
- - -
PUP-Loki -
-
- - -
Daniel Sörlöv -
-
- - -
Theyooo -
-
- - -
Justin Peacock -
-
- - -
Chris Tracy -
-
- - -
Fuechslein -
-
- - -
Amir Zarrinkafsh -
-
- - -
gabbe -
-
- - -
bmbvenom -
-
- - -
Florian Meinicke -
-
- - -
Rahul Somasundaram -
-
- - -
Björn Heinrichs -
-
- - -
Josh Byrnes -
-
- - -
bergi9 -
-
- - -
luoweihua7 -
-
- - -
Tobias Kneidl -
-
- - -
Pius Walter -
-
- - -
Troy Kelly -
-
- - -
Ivan Kristianto -
-
- - -
Omer Cohen -
-
- - +A guide to setting up your own development environment [is found here](DEV-README.md). diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 000000000..8b96428f4 --- /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 6d6172a48..000000000 --- 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 149080b91..000000000 --- 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 000000000..7f0d08570 --- /dev/null +++ b/backend/.golangci.yml @@ -0,0 +1,166 @@ +--- +linters: + enable: + # Prevents against memory leaks in production caused by not closing + # file handle + - bodyclose + # 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 + # Controls Go package import order and makes it always deterministic. + - gci + # 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 + # Detects 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. + - revive + # Detects ineffectual assignments in code + - ineffassign + # Reports long lines + # - lll + # 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 + # Remove unnecessary type conversions + - unconvert + # Remove unnecessary(unused) function parameters + - unparam +linters-settings: + errcheck: + exclude-functions: + - fmt.Fprint + - fmt.Fprintf + gci: + sections: + - standard # Standard section: captures all standard packages. + - localmodule # Local module section: contains all local packages. + # - prefix(gogs.jc21.com) # Prefixed gerrit.lan packages (jumgo). + - default # Everything else (github.com, golang.org, etc). + - blank # Blank section: contains all blank imports. + custom-order: true + goconst: + # minimal length of string constant + # default: 3 + min-len: 2 + # minimum number of occurrences of string constant + # default: 3 + min-occurences: 2 + revive: + enable-all-rules: true + rules: + - name: unchecked-type-assertion + disabled: true + # handled by goconst + - name: add-constant + disabled: true + # cant limit this arbitrarily + - name: argument-limit + disabled: true + # handled by gocyclo + - name: cognitive-complexity + disabled: true + # false positive for Exported vs non-exported functions of the same name + - name: confusing-naming + disabled: true + # false positives for "" - which is the nil value of a string (also 0) + - name: confusing-results + disabled: true + # handled by gocyclo + - name: cyclomatic + disabled: true + # have comments on exported functions but not on vars/types/constants + - name: exported + arguments: + - "disableChecksOnConstants" + - "disableChecksOnTypes" + - "disableChecksOnVariables" + # false positives on bool params + - name: flag-parameter + disabled: true + # extreme verticalization can happen + - name: function-length + disabled: true + # can false positive for non-getters + - name: get-return + disabled: true + # only allows lowercase names + - name: import-alias-naming + disabled: true + # handled by lll + - name: line-length-limit + disabled: true + # don't want to arbitrarily limit this + # many places have specific model.go files to contain all structs + - name: max-public-structs + disabled: true + # disable package-comments + - name: package-comments + disabled: true + # this is handled by errcheck + - name: unhandled-error + disabled: true + - name: function-result-limit + disabled: true +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 + # Test data can long + # - lll +run: + go: '1.23' diff --git a/backend/.prettierrc b/backend/.prettierrc deleted file mode 100644 index fefbcfa6d..000000000 --- a/backend/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "printWidth": 320, - "tabWidth": 4, - "useTabs": true, - "semi": true, - "singleQuote": true, - "bracketSpacing": true, - "jsxBracketSameLine": true, - "trailingComma": "all", - "proseWrap": "always" -} diff --git a/backend/.testcoverage.yml b/backend/.testcoverage.yml new file mode 100644 index 000000000..b52eaeffe --- /dev/null +++ b/backend/.testcoverage.yml @@ -0,0 +1,21 @@ +--- + +# (mandatory) +# Path to coverprofile file (output of `go test -coverprofile` command). +profile: coverage.out + +# (optional; but recommended to set) +# When specified reported file paths will not contain local prefix in the output +local-prefix: "npm" + +# Holds coverage thresholds percentages, values should be in range [0-100] +threshold: + # (optional; default 0) + # The minimum coverage that each file should have + # file: 70 + # (optional; default 0) + # The minimum coverage that each package should have + # package: 30 + # (optional; default 0) + # The minimum total coverage project should have + total: 30 diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json deleted file mode 100644 index 4e540ab30..000000000 --- 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/README.md b/backend/README.md new file mode 100644 index 000000000..912f00690 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,6 @@ +# Backend + +## Guides and materials + +- [Nginx Proxy Protocol](https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/) +- diff --git a/backend/Taskfile.yml b/backend/Taskfile.yml new file mode 100644 index 000000000..512046e38 --- /dev/null +++ b/backend/Taskfile.yml @@ -0,0 +1,70 @@ +version: "3" + +tasks: + default: + cmds: + - task: run + + run: + desc: Build and run + sources: + - internal/**/*.go + - cmd/**/*.go + - ../frontend/src/locale/src/*.json + cmds: + - task: locale + - task: build + force: true + - 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 -tags 'json1' -buildvcs=false -ldflags="-X main.commit={{.GIT_COMMIT}} -X main.version={{.VERSION}}" -o ../dist/bin/server ./cmd/server/main.go + silent: true + - cmd: go build -buildvcs=false -ldflags="-X main.commit={{.GIT_COMMIT}} -X main.version={{.VERSION}}" -o ../dist/bin/ipranges ./cmd/ipranges/main.go + silent: true + - cmd: rm -f /etc/nginx/conf.d/include/ipranges.conf && /app/dist/bin/ipranges > /etc/nginx/conf.d/include/ipranges.conf + - task: lint + vars: + GIT_COMMIT: + sh: git log -n 1 --format=%h + VERSION: + sh: cat ../.version + + 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 + + locale: + desc: Locale + dir: /app/frontend + cmds: + - cmd: yarn locale-compile + silent: true + ignore_error: true + - cmd: chown -R "$PUID:$PGID" src/locale/lang + silent: true + ignore_error: true diff --git a/backend/app.js b/backend/app.js deleted file mode 100644 index ca6d6fbae..000000000 --- a/backend/app.js +++ /dev/null @@ -1,89 +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({ - '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' || process.env.DEBUG) { - 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/ipranges/main.go b/backend/cmd/ipranges/main.go new file mode 100644 index 000000000..7aaceccfd --- /dev/null +++ b/backend/cmd/ipranges/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "npm/internal/config" + "npm/internal/model" + + "github.com/rotisserie/eris" +) + +var commit string +var version string + +var cloudfrontURL = "https://ip-ranges.amazonaws.com/ip-ranges.json" +var cloudflare4URL = "https://www.cloudflare.com/ips-v4" +var cloudflare6URL = "https://www.cloudflare.com/ips-v6" + +func main() { + config.InitArgs(&version, &commit) + if err := config.InitIPRanges(&version, &commit); err != nil { + fmt.Printf("# Config ERROR: %v\n", err) + os.Exit(1) + } + + exitCode := 0 + + // Cloudfront + fmt.Printf("# Cloudfront Ranges from: %s\n", cloudfrontURL) + if ranges, err := parseCloudfront(); err == nil { + for _, item := range ranges { + fmt.Printf("set_real_ip_from %s;\n", item) + } + } else { + fmt.Printf("# ERROR: %v\n", err) + } + + // Cloudflare ipv4 + if !config.Configuration.DisableIPV4 { + fmt.Printf("\n# Cloudflare Ranges from: %s\n", cloudflare4URL) + if ranges, err := parseCloudflare(cloudflare4URL); err == nil { + for _, item := range ranges { + fmt.Printf("set_real_ip_from %s;\n", item) + } + } else { + fmt.Printf("# ERROR: %v\n", err) + } + } + + // Cloudflare ipv6 + if !config.Configuration.DisableIPV6 { + fmt.Printf("\n# Cloudflare Ranges from: %s\n", cloudflare6URL) + if ranges, err := parseCloudflare(cloudflare6URL); err == nil { + for _, item := range ranges { + fmt.Printf("set_real_ip_from %s;\n", item) + } + } else { + fmt.Printf("# ERROR: %v\n", err) + } + } + + // Done + os.Exit(exitCode) +} + +func parseCloudfront() ([]string, error) { + // nolint: gosec + resp, err := http.Get(cloudfrontURL) + if err != nil { + return nil, eris.Wrapf(err, "Failed to download Cloudfront IP Ranges from %s", cloudfrontURL) + } + + // nolint: errcheck, gosec + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, eris.Wrapf(err, "Failed to read Cloudfront IP Ranges body") + } + + var result model.CloudfrontIPRanges + if err := json.Unmarshal(body, &result); err != nil { + return nil, eris.Wrapf(err, "Failed to unmarshal Cloudfront IP Ranges file") + } + + ranges := make([]string, 0) + if !config.Configuration.DisableIPV4 { + for _, item := range result.IPV4Prefixes { + ranges = append(ranges, item.Value) + } + } + if !config.Configuration.DisableIPV6 { + for _, item := range result.IPV6Prefixes { + ranges = append(ranges, item.Value) + } + } + + return ranges, nil +} + +func parseCloudflare(url string) ([]string, error) { + // nolint: gosec + resp, err := http.Get(url) + if err != nil { + return nil, eris.Wrapf(err, "Failed to download Cloudflare IP Ranges from %s", url) + } + + // nolint: errcheck, gosec + defer resp.Body.Close() + + scanner := bufio.NewScanner(resp.Body) + scanner.Split(bufio.ScanLines) + + ranges := make([]string, 0) + for scanner.Scan() { + if scanner.Text() != "" { + ranges = append(ranges, scanner.Text()) + } + } + return ranges, nil +} diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 000000000..7f6e93a02 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "npm/internal/api" + "npm/internal/config" + "npm/internal/database" + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/user" + "npm/internal/errors" + "npm/internal/jobqueue" + "npm/internal/jwt" + "npm/internal/logger" + + // properly respect available cpu cores + _ "go.uber.org/automaxprocs" +) + +var commit string +var version string + +func main() { + config.InitArgs(&version, &commit) + config.Init(&version, &commit) + config.CreateDataFolders() + logger.Info("Build Version: %s (%s)", version, commit) + + database.Migrate(func() { + if err := jwt.LoadKeys(); err != nil { + logger.Error("KeysError", err) + os.Exit(1) + } + + checkSetup() + + // Internal Job Queue + jobqueue.Start() + certificate.AddPendingJobs() + host.AddPendingJobs() + + // Http server + 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 + sqlDB, _ := database.GetDB().DB() + err := sqlDB.Close() + if err != nil { + logger.Error("DatabaseCloseError", err) + } + // nolint + jobqueue.Shutdown() + break + } + } + }) +} + +// checkSetup Quick check by counting the number of users in the database +func checkSetup() { + db := database.GetDB() + var count int64 + + if db != nil { + db.Model(&user.Model{}). + Where("is_disabled = ?", false). + Where("is_system = ?", false). + Count(&count) + + if count == 0 { + logger.Warn("No users found, starting in Setup Mode") + } else { + config.IsSetup = true + logger.Info("Application is setup") + } + } else { + logger.Error("DatabaseError", errors.ErrDatabaseUnavailable) + } +} diff --git a/backend/config/README.md b/backend/config/README.md deleted file mode 100644 index 26268a116..000000000 --- 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 64ab577c8..000000000 --- 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 ad5488651..000000000 --- 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 ce5338f01..000000000 --- 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 deleted file mode 100644 index 06c025648..000000000 --- a/backend/doc/api.swagger.json +++ /dev/null @@ -1,1254 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Nginx Proxy Manager API", - "version": "2.x.x" - }, - "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" - } - } - } - } - } - } - }, - "/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" - ] - } - }, - "/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" - } - } - } - } - } - } - }, - "/settings/{settingID}": { - "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" - } - } - } - } - } - }, - "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" - } - } - } - } - } - } - }, - "/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" - } - } - } - } - } - }, - "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" - } - } - } - } - } - } - }, - "/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" - } - } - } - } - } - }, - "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" - } - } - } - } - } - }, - "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" - } - } - } - } - } - } - }, - "/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" - } - } - } - } - } - } - } - }, - "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 - } - } - } - } - }, - "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" - } - } - }, - "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" - } - } - }, - "SettingsList": { - "type": "array", - "description": "Setting list", - "items": { - "$ref": "#/components/schemas/SettingObject" - } - }, - "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" - } - } - } - } - } -} \ No newline at end of file diff --git a/backend/embed/api_docs/api.swagger.json b/backend/embed/api_docs/api.swagger.json new file mode 100644 index 000000000..7946b9cb5 --- /dev/null +++ b/backend/embed/api_docs/api.swagger.json @@ -0,0 +1,310 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Nginx Proxy Manager API", + "version": "{{VERSION}}" + }, + "paths": { + "/": { + "get": { + "$ref": "file://./paths/get.json" + } + }, + "/auth": { + "get": { + "$ref": "file://./paths/auth/get.json" + }, + "post": { + "$ref": "file://./paths/auth/post.json" + } + }, + "/auth/refresh": { + "post": { + "$ref": "file://./paths/auth/refresh/post.json" + } + }, + "/auth/sse": { + "post": { + "$ref": "file://./paths/auth/sse/post.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" + } + }, + "/hosts/{hostID}/nginx-config": { + "get": { + "$ref": "file://./paths/hosts/hostID/nginx-config/get.json" + } + }, + "/nginx-templates": { + "get": { + "$ref": "file://./paths/nginx-templates/get.json" + }, + "post": { + "$ref": "file://./paths/nginx-templates/post.json" + } + }, + "/nginx-templates/{templateID}": { + "get": { + "$ref": "file://./paths/nginx-templates/templateID/get.json" + }, + "put": { + "$ref": "file://./paths/nginx-templates/templateID/put.json" + }, + "delete": { + "$ref": "file://./paths/nginx-templates/templateID/delete.json" + } + }, + "/schema": { + "get": { + "$ref": "file://./paths/schema/get.json" + } + }, + "/settings": { + "get": { + "$ref": "file://./paths/settings/get.json" + }, + "post": { + "$ref": "file://./paths/settings/post.json" + } + }, + "/settings/{name}": { + "get": { + "$ref": "file://./paths/settings/name/get.json" + }, + "put": { + "$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" + } + }, + "/upstreams": { + "get": { + "$ref": "file://./paths/upstreams/get.json" + }, + "post": { + "$ref": "file://./paths/upstreams/post.json" + } + }, + "/upstreams/{upstreamID}": { + "get": { + "$ref": "file://./paths/upstreams/upstreamID/get.json" + }, + "put": { + "$ref": "file://./paths/upstreams/upstreamID/put.json" + }, + "delete": { + "$ref": "file://./paths/upstreams/upstreamID/delete.json" + } + }, + "/upstreams/{upstreamID}/nginx-config": { + "get": { + "$ref": "file://./paths/upstreams/upstreamID/nginx-config/get.json" + } + }, + "/users": { + "get": { + "$ref": "file://./paths/users/get.json" + }, + "post": { + "$ref": "file://./paths/users/post.json" + } + }, + "/users/{userID}": { + "get": { + "$ref": "file://./paths/users/userID/get.json" + }, + "put": { + "$ref": "file://./paths/users/userID/put.json" + }, + "delete": { + "$ref": "file://./paths/users/userID/delete.json" + } + }, + "/users/{userID}/auth": { + "post": { + "$ref": "file://./paths/users/userID/auth/post.json" + } + } + }, + "components": { + "schemas": { + "AuthConfigObject": { + "$ref": "file://./components/AuthConfigObject.json" + }, + "CertificateAuthorityList": { + "$ref": "file://./components/CertificateAuthorityList.json" + }, + "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" + }, + "NginxTemplateList": { + "$ref": "file://./components/NginxTemplateList.json" + }, + "NginxTemplateObject": { + "$ref": "file://./components/NginxTemplateObject.json" + }, + "SettingList": { + "$ref": "file://./components/SettingList.json" + }, + "SettingObject": { + "$ref": "file://./components/SettingObject.json" + }, + "SortObject": { + "$ref": "file://./components/SortObject.json" + }, + "StreamList": { + "$ref": "file://./components/StreamList.json" + }, + "StreamObject": { + "$ref": "file://./components/StreamObject.json" + }, + "TokenObject": { + "$ref": "file://./components/TokenObject.json" + }, + "UpstreamList": { + "$ref": "file://./components/UpstreamList.json" + }, + "UpstreamObject": { + "$ref": "file://./components/UpstreamObject.json" + }, + "UserAuthObject": { + "$ref": "file://./components/UserAuthObject.json" + }, + "UserList": { + "$ref": "file://./components/UserList.json" + }, + "UserObject": { + "$ref": "file://./components/UserObject.json" + } + } + } +} diff --git a/backend/embed/api_docs/components/AuthConfigObject.json b/backend/embed/api_docs/components/AuthConfigObject.json new file mode 100644 index 000000000..9a6aa3969 --- /dev/null +++ b/backend/embed/api_docs/components/AuthConfigObject.json @@ -0,0 +1,13 @@ +{ + "type": "array", + "description": "AuthConfigObject", + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "local", + "ldap", + "oauth" + ] + } +} diff --git a/backend/embed/api_docs/components/CertificateAuthorityList.json b/backend/embed/api_docs/components/CertificateAuthorityList.json new file mode 100644 index 000000000..131140ef8 --- /dev/null +++ b/backend/embed/api_docs/components/CertificateAuthorityList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/CertificateAuthorityObject.json b/backend/embed/api_docs/components/CertificateAuthorityObject.json new file mode 100644 index 000000000..7cd157f20 --- /dev/null +++ b/backend/embed/api_docs/components/CertificateAuthorityObject.json @@ -0,0 +1,57 @@ +{ + "type": "object", + "description": "CertificateAuthorityObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "name", + "acmesh_server", + "ca_bundle", + "max_domains", + "is_wildcard_supported", + "is_readonly" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "acmesh_server": { + "type": "string", + "minLength": 2, + "maxLength": 255 + }, + "ca_bundle": { + "type": "string", + "minLength": 0, + "maxLength": 255 + }, + "max_domains": { + "type": "integer", + "minimum": 1 + }, + "is_wildcard_supported": { + "type": "boolean" + }, + "is_readonly": { + "type": "boolean" + } + } +} diff --git a/backend/embed/api_docs/components/CertificateList.json b/backend/embed/api_docs/components/CertificateList.json new file mode 100644 index 000000000..8fbf2cccf --- /dev/null +++ b/backend/embed/api_docs/components/CertificateList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/CertificateObject.json b/backend/embed/api_docs/components/CertificateObject.json new file mode 100644 index 000000000..18a087f5b --- /dev/null +++ b/backend/embed/api_docs/components/CertificateObject.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "description": "CertificateObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "expires_on", + "type", + "user_id", + "certificate_authority_id", + "dns_provider_id", + "name", + "is_ecc", + "status", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "expires_on": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "type": { + "type": "string", + "enum": ["custom", "http", "dns"] + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "certificate_authority_id": { + "type": "integer", + "minimum": 0 + }, + "certificate_authority": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + }, + "dns_provider_id": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "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"] + }, + "is_ecc": { + "type": "boolean" + }, + "error_message": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserObject" + } + } +} diff --git a/backend/embed/api_docs/components/ConfigObject.json b/backend/embed/api_docs/components/ConfigObject.json new file mode 100644 index 000000000..96c4176f8 --- /dev/null +++ b/backend/embed/api_docs/components/ConfigObject.json @@ -0,0 +1,4 @@ +{ + "type": "object", + "description": "ConfigObject" +} diff --git a/backend/embed/api_docs/components/DNSProviderList.json b/backend/embed/api_docs/components/DNSProviderList.json new file mode 100644 index 000000000..edf8385ce --- /dev/null +++ b/backend/embed/api_docs/components/DNSProviderList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/DNSProviderObject.json b/backend/embed/api_docs/components/DNSProviderObject.json new file mode 100644 index 000000000..2e1f305e2 --- /dev/null +++ b/backend/embed/api_docs/components/DNSProviderObject.json @@ -0,0 +1,51 @@ +{ + "type": "object", + "description": "DNSProviderObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "user_id", + "name", + "acmesh_name", + "dns_sleep", + "meta" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "acmesh_name": { + "type": "string", + "minLength": 4, + "maxLength": 50 + }, + "dns_sleep": { + "type": "integer" + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/embed/api_docs/components/DeletedItemResponse.json b/backend/embed/api_docs/components/DeletedItemResponse.json new file mode 100644 index 000000000..0e0a9a0eb --- /dev/null +++ b/backend/embed/api_docs/components/DeletedItemResponse.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "description": "DeletedItemResponse", + "additionalProperties": false, + "required": ["result"], + "properties": { + "result": { + "type": "boolean", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } +} diff --git a/backend/embed/api_docs/components/ErrorObject.json b/backend/embed/api_docs/components/ErrorObject.json new file mode 100644 index 000000000..a1d776058 --- /dev/null +++ b/backend/embed/api_docs/components/ErrorObject.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "ErrorObject", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer", + "description": "Error code", + "minimum": 0 + }, + "message": { + "type": "string", + "description": "Error message" + } + } +} diff --git a/backend/embed/api_docs/components/FilterObject.json b/backend/embed/api_docs/components/FilterObject.json new file mode 100644 index 000000000..4ba75766d --- /dev/null +++ b/backend/embed/api_docs/components/FilterObject.json @@ -0,0 +1,24 @@ +{ + "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/embed/api_docs/components/HealthObject.json b/backend/embed/api_docs/components/HealthObject.json new file mode 100644 index 000000000..cf2897a72 --- /dev/null +++ b/backend/embed/api_docs/components/HealthObject.json @@ -0,0 +1,31 @@ +{ + "type": "object", + "description": "HealthObject", + "additionalProperties": false, + "required": ["version", "commit", "healthy", "setup"], + "properties": { + "version": { + "type": "string", + "description": "Version", + "minLength": 1 + }, + "commit": { + "type": "string", + "description": "Commit hash", + "minLength": 7 + }, + "healthy": { + "type": "boolean", + "description": "Healthy?" + }, + "setup": { + "type": "boolean", + "description": "Is the application set up?" + }, + "acme.sh": { + "type": "string", + "description": "Acme.sh version", + "minLength": 1 + } + } +} diff --git a/backend/embed/api_docs/components/HostList.json b/backend/embed/api_docs/components/HostList.json new file mode 100644 index 000000000..8d9d413a8 --- /dev/null +++ b/backend/embed/api_docs/components/HostList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/HostObject.json b/backend/embed/api_docs/components/HostObject.json new file mode 100644 index 000000000..8b166ead0 --- /dev/null +++ b/backend/embed/api_docs/components/HostObject.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "description": "HostObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "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 + } + }, + "user": { + "$ref": "#/components/schemas/UserObject" + } + } +} diff --git a/backend/embed/api_docs/components/NginxTemplateList.json b/backend/embed/api_docs/components/NginxTemplateList.json new file mode 100644 index 000000000..c0c11be3a --- /dev/null +++ b/backend/embed/api_docs/components/NginxTemplateList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "NginxTemplateList", + "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/NginxTemplateObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/NginxTemplateObject.json b/backend/embed/api_docs/components/NginxTemplateObject.json new file mode 100644 index 000000000..9b6b3f86c --- /dev/null +++ b/backend/embed/api_docs/components/NginxTemplateObject.json @@ -0,0 +1,46 @@ +{ + "type": "object", + "description": "NginxTemplateObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "user_id", + "name", + "type", + "template" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "pattern": "^proxy|redirect|dead|stream|upstream$" + }, + "template": { + "type": "string", + "minLength": 20 + } + } +} diff --git a/backend/embed/api_docs/components/SettingList.json b/backend/embed/api_docs/components/SettingList.json new file mode 100644 index 000000000..77fd564be --- /dev/null +++ b/backend/embed/api_docs/components/SettingList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/SettingObject.json b/backend/embed/api_docs/components/SettingObject.json new file mode 100644 index 000000000..6f151dfe7 --- /dev/null +++ b/backend/embed/api_docs/components/SettingObject.json @@ -0,0 +1,51 @@ +{ + "type": "object", + "description": "SettingObject", + "additionalProperties": false, + "required": ["id", "created_at", "updated_at", "name", "value"], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "description": { + "type": "string", + "minLength": 0, + "maxLength": 100 + }, + "value": { + "oneOf": [ + { + "type": "array" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } +} diff --git a/backend/embed/api_docs/components/SortObject.json b/backend/embed/api_docs/components/SortObject.json new file mode 100644 index 000000000..a2810ce61 --- /dev/null +++ b/backend/embed/api_docs/components/SortObject.json @@ -0,0 +1,17 @@ +{ + "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/embed/api_docs/components/StreamList.json b/backend/embed/api_docs/components/StreamList.json new file mode 100644 index 000000000..c3dae5ab6 --- /dev/null +++ b/backend/embed/api_docs/components/StreamList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/StreamObject.json b/backend/embed/api_docs/components/StreamObject.json new file mode 100644 index 000000000..9c8787194 --- /dev/null +++ b/backend/embed/api_docs/components/StreamObject.json @@ -0,0 +1,57 @@ +{ + "type": "object", + "description": "StreamObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "expires_on", + "user_id", + "provider", + "name", + "domain_names" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "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 + } + } + } +} diff --git a/backend/embed/api_docs/components/TokenObject.json b/backend/embed/api_docs/components/TokenObject.json new file mode 100644 index 000000000..88863f4c7 --- /dev/null +++ b/backend/embed/api_docs/components/TokenObject.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "description": "TokenObject", + "additionalProperties": false, + "required": ["expires", "token"], + "properties": { + "expires": { + "type": "number", + "description": "Token Expiry Unix Time", + "minimum": 1 + }, + "token": { + "type": "string", + "description": "JWT Token" + } + } +} diff --git a/backend/embed/api_docs/components/UpstreamList.json b/backend/embed/api_docs/components/UpstreamList.json new file mode 100644 index 000000000..316725ac5 --- /dev/null +++ b/backend/embed/api_docs/components/UpstreamList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "UpstreamList", + "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/UpstreamObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/UpstreamObject.json b/backend/embed/api_docs/components/UpstreamObject.json new file mode 100644 index 000000000..5a64e18a0 --- /dev/null +++ b/backend/embed/api_docs/components/UpstreamObject.json @@ -0,0 +1,140 @@ +{ + "type": "object", + "description": "UpstreamObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "user_id", + "name", + "nginx_template_id", + "ip_hash", + "ntlm", + "keepalive", + "keepalive_requests", + "keepalive_time", + "keepalive_timeout", + "advanced_config", + "status", + "error_message", + "servers" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "ip_hash": { + "type": "boolean" + }, + "ntlm": { + "type": "boolean" + }, + "keepalive": { + "type": "integer" + }, + "keepalive_requests": { + "type": "integer" + }, + "keepalive_time": { + "type": "string" + }, + "keepalive_timeout": { + "type": "string" + }, + "advanced_config": { + "type": "string" + }, + "status": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserObject" + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "upstream_id", + "server", + "weight", + "max_conns", + "max_fails", + "fail_timeout", + "backup" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "upstream_id": { + "type": "integer", + "minimum": 1 + }, + "server": { + "type": "string", + "minLength": 2 + }, + "weight": { + "type": "integer" + }, + "max_conns": { + "type": "integer" + }, + "max_fails": { + "type": "integer" + }, + "fail_timeout": { + "type": "integer" + }, + "backup": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/embed/api_docs/components/UserAuthObject.json b/backend/embed/api_docs/components/UserAuthObject.json new file mode 100644 index 000000000..1059b43cd --- /dev/null +++ b/backend/embed/api_docs/components/UserAuthObject.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "description": "UserAuthObject", + "additionalProperties": false, + "required": ["id", "user_id", "type", "created_at", "updated_at"], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "type": { + "type": "string", + "pattern": "^(local|ldap|oauth)$" + } + } +} diff --git a/backend/embed/api_docs/components/UserList.json b/backend/embed/api_docs/components/UserList.json new file mode 100644 index 000000000..a4d502f35 --- /dev/null +++ b/backend/embed/api_docs/components/UserList.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } +} diff --git a/backend/embed/api_docs/components/UserObject.json b/backend/embed/api_docs/components/UserObject.json new file mode 100644 index 000000000..94a6dc5d1 --- /dev/null +++ b/backend/embed/api_docs/components/UserObject.json @@ -0,0 +1,69 @@ +{ + "type": "object", + "description": "UserObject", + "additionalProperties": false, + "required": [ + "id", + "created_at", + "updated_at", + "name", + "email", + "is_disabled" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_at": { + "type": "integer", + "minimum": 1, + "description": "Created Unix time with milliseconds" + }, + "updated_at": { + "type": "integer", + "minimum": 1, + "description": "Updated Unix time with milliseconds" + }, + "name": { + "type": "string", + "minLength": 2, + "maxLength": 50 + }, + "email": { + "type": "string", + "minLength": 5, + "maxLength": 150 + }, + "gravatar_url": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + }, + "is_system": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": ["type"], + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string", + "pattern": "^(local|ldap|oauth)$" + } + } + }, + "capabilities": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + } +} diff --git a/backend/embed/api_docs/paths/auth/get.json b/backend/embed/api_docs/paths/auth/get.json new file mode 100644 index 000000000..e34da6c34 --- /dev/null +++ b/backend/embed/api_docs/paths/auth/get.json @@ -0,0 +1,28 @@ +{ + "operationId": "getAuthConfig", + "summary": "Returns auth configuration", + "tags": ["Auth"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/AuthConfigObject" + } + } + }, + "examples": { + "default": { + "value": "todo" + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/auth/post.json b/backend/embed/api_docs/paths/auth/post.json new file mode 100644 index 000000000..2cd55d3e6 --- /dev/null +++ b/backend/embed/api_docs/paths/auth/post.json @@ -0,0 +1,75 @@ +{ + "operationId": "requestToken", + "summary": "Request a new access token from credentials", + "tags": ["Auth"], + "requestBody": { + "description": "Credentials Payload", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.GetToken}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/TokenObject" + } + } + }, + "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": { + "type": "object", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 403, + "message": "Not available during setup phase" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/auth/refresh/post.json b/backend/embed/api_docs/paths/auth/refresh/post.json new file mode 100644 index 000000000..b44bf95c9 --- /dev/null +++ b/backend/embed/api_docs/paths/auth/refresh/post.json @@ -0,0 +1,34 @@ +{ + "operationId": "refreshToken", + "summary": "Refresh your access token", + "tags": ["Auth"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/TokenObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/auth/sse/post.json b/backend/embed/api_docs/paths/auth/sse/post.json new file mode 100644 index 000000000..0e53181aa --- /dev/null +++ b/backend/embed/api_docs/paths/auth/sse/post.json @@ -0,0 +1,34 @@ +{ + "operationId": "requestSSEToken", + "summary": "Request a new SSE token", + "tags": ["Auth"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/TokenObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "expires": 1566540510, + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "scope": "user" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/caID/delete.json b/backend/embed/api_docs/paths/certificates-authorities/caID/delete.json new file mode 100644 index 000000000..3ae3bea87 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/certificates-authorities/caID/get.json b/backend/embed/api_docs/paths/certificates-authorities/caID/get.json new file mode 100644 index 000000000..2034faaab --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/caID/get.json @@ -0,0 +1,53 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1627531400000, + "updated_at": 1627531400000, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/caID/put.json b/backend/embed/api_docs/paths/certificates-authorities/caID/put.json new file mode 100644 index 000000000..0e18611e0 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/caID/put.json @@ -0,0 +1,62 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1627531400000, + "updated_at": 1627531400000, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/get.json b/backend/embed/api_docs/paths/certificates-authorities/get.json new file mode 100644 index 000000000..f2e1d4c7c --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/get.json @@ -0,0 +1,93 @@ +{ + "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": { + "type": "object", + "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_at": 1627531400000, + "updated_at": 1627531400000, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_setup": true + }, + { + "id": 2, + "created_at": 1627531400000, + "updated_at": 1627531400000, + "name": "Let's Encrypt", + "acmesh_server": "https://acme-v02.api.letsencrypt.org/directory", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_setup": true + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates-authorities/post.json b/backend/embed/api_docs/paths/certificates-authorities/post.json new file mode 100644 index 000000000..bac0df52e --- /dev/null +++ b/backend/embed/api_docs/paths/certificates-authorities/post.json @@ -0,0 +1,49 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateAuthorityObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1627531400000, + "updated_at": 1627531400000, + "name": "ZeroSSL", + "acmesh_server": "zerossl", + "ca_bundle": "", + "max_domains": 10, + "is_wildcard_supported": true, + "is_readonly": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates/certificateID/delete.json b/backend/embed/api_docs/paths/certificates/certificateID/delete.json new file mode 100644 index 000000000..98acfaf77 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/certificates/certificateID/get.json b/backend/embed/api_docs/paths/certificates/certificateID/get.json new file mode 100644 index 000000000..637e1e191 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/certificateID/get.json @@ -0,0 +1,56 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1604536109000, + "updated_at": 1604536109000, + "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"], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates/certificateID/put.json b/backend/embed/api_docs/paths/certificates/certificateID/put.json new file mode 100644 index 000000000..011961532 --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/certificateID/put.json @@ -0,0 +1,65 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1604536109000, + "updated_at": 1604536109000, + "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"], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates/get.json b/backend/embed/api_docs/paths/certificates/get.json new file mode 100644 index 000000000..b3113e67a --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/get.json @@ -0,0 +1,87 @@ +{ + "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": { + "type": "object", + "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_at": 1604536109000, + "updated_at": 1604536109000, + "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" + ], + "is_ecc": 0, + "status": "ready" + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/certificates/post.json b/backend/embed/api_docs/paths/certificates/post.json new file mode 100644 index 000000000..7b93d9aff --- /dev/null +++ b/backend/embed/api_docs/paths/certificates/post.json @@ -0,0 +1,52 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/CertificateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1604536109000, + "updated_at": 1604536109000, + "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"], + "is_ecc": 0, + "status": "ready" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/config/get.json b/backend/embed/api_docs/paths/config/get.json new file mode 100644 index 000000000..05a083b2e --- /dev/null +++ b/backend/embed/api_docs/paths/config/get.json @@ -0,0 +1,35 @@ +{ + "operationId": "config", + "summary": "Returns the API Service configuration", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/ConfigObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "data": "/data", + "log": { + "level": "debug", + "format": "nice" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/get.json b/backend/embed/api_docs/paths/dns-providers/get.json new file mode 100644 index 000000000..c3e281d72 --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/get.json @@ -0,0 +1,83 @@ +{ + "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": { + "type": "object", + "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_at": 1602593653000, + "updated_at": 1602593653000, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/post.json b/backend/embed/api_docs/paths/dns-providers/post.json new file mode 100644 index 000000000..f7d142a8d --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/post.json @@ -0,0 +1,50 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1602593653000, + "updated_at": 1602593653000, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/providerID/delete.json b/backend/embed/api_docs/paths/dns-providers/providerID/delete.json new file mode 100644 index 000000000..32b77b0d1 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/dns-providers/providerID/get.json b/backend/embed/api_docs/paths/dns-providers/providerID/get.json new file mode 100644 index 000000000..743366021 --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/providerID/get.json @@ -0,0 +1,54 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1602593653000, + "updated_at": 1602593653000, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/dns-providers/providerID/put.json b/backend/embed/api_docs/paths/dns-providers/providerID/put.json new file mode 100644 index 000000000..b5709994d --- /dev/null +++ b/backend/embed/api_docs/paths/dns-providers/providerID/put.json @@ -0,0 +1,65 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/DNSProviderObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "result": { + "id": 1, + "created_at": 1602593653000, + "updated_at": 1602593653000, + "user_id": 1, + "name": "Route53", + "acmesh_name": "dns_aws", + "meta": { + "AWS_ACCESS_KEY_ID": "abc123", + "AWS_SECRET_ACCESS_KEY": "def098" + } + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/get.json b/backend/embed/api_docs/paths/get.json new file mode 100644 index 000000000..567b2e78a --- /dev/null +++ b/backend/embed/api_docs/paths/get.json @@ -0,0 +1,44 @@ +{ + "operationId": "health", + "summary": "Returns the API health status", + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HealthObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": true, + "setup": true + } + } + }, + "unhealthy": { + "value": { + "result": { + "version": "3.0.0", + "commit": "9f119b6", + "healthy": false, + "setup": true + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/get.json b/backend/embed/api_docs/paths/hosts/get.json new file mode 100644 index 000000000..ae82a57de --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/get.json @@ -0,0 +1,94 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "domain_names", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_at": 1646279455000, + "updated_at": 1646279455000, + "user_id": 2, + "type": "proxy", + "nginx_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "advanced_config": "", + "is_disabled": false + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/hostID/delete.json b/backend/embed/api_docs/paths/hosts/hostID/delete.json new file mode 100644 index 000000000..4df119ad0 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/hosts/hostID/get.json b/backend/embed/api_docs/paths/hosts/hostID/get.json new file mode 100644 index 000000000..0f2ca20d4 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/get.json @@ -0,0 +1,65 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1646279455000, + "updated_at": 1646279455000, + "user_id": 2, + "type": "proxy", + "nginx_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/hostID/nginx-config/get.json b/backend/embed/api_docs/paths/hosts/hostID/nginx-config/get.json new file mode 100644 index 000000000..ea7be7ab7 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/nginx-config/get.json @@ -0,0 +1,43 @@ +{ + "operationId": "getHostNginxConfig", + "summary": "Get a Host Nginx Config 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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "type": "string" + } + } + }, + "examples": { + "default": { + "value": { + "result": "# ------------------------------------------------------------\n# a.example.com\n# ------------------------------------------------------------\nserver {\n listen 80;\n server_name a.example.com ;\n access_log /data/logs/host-1_access.log proxy;\n error_log /data/logs/host-1_error.log warn;\n # locations ?\n # default location:\n location / {\n # Access Rules ? todo\n # Access checks must...? todo\n # Proxy!\n add_header X-Served-By $host;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Scheme $scheme;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-For $remote_addr;\n proxy_http_version 1.1;\n # proxy a single host\n proxy_pass http://192.168.0.10:80;\n }\n # Legacy Custom Configuration\n include /data/nginx/custom/server_proxy[.]conf;\n}\n" + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/hostID/put.json b/backend/embed/api_docs/paths/hosts/hostID/put.json new file mode 100644 index 000000000..cff143bed --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/hostID/put.json @@ -0,0 +1,74 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1646279455000, + "updated_at": 1646279455000, + "user_id": 2, + "type": "proxy", + "nginx_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/post.json b/backend/embed/api_docs/paths/hosts/post.json new file mode 100644 index 000000000..6e9d605e2 --- /dev/null +++ b/backend/embed/api_docs/paths/hosts/post.json @@ -0,0 +1,61 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/HostObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1645700556000, + "updated_at": 1645700556000, + "user_id": 2, + "type": "proxy", + "nginx_template_id": 1, + "listen_interface": "", + "domain_names": ["jc21.com"], + "upstream_id": 0, + "certificate_id": 0, + "access_list_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "paths": "", + "advanced_config": "", + "is_disabled": false + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/nginx-templates/get.json b/backend/embed/api_docs/paths/nginx-templates/get.json new file mode 100644 index 000000000..89f389e55 --- /dev/null +++ b/backend/embed/api_docs/paths/nginx-templates/get.json @@ -0,0 +1,80 @@ +{ + "operationId": "getNginxTemplates", + "summary": "Get a list of Nginx Templates", + "tags": ["Nginx Templates"], + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/NginxTemplateList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 1, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "created_at", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_at": 1646218093000, + "updated_at": 1646218093000, + "user_id": 1, + "name": "Default Proxy Template", + "type": "proxy", + "template": "# this is a proxy template" + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/nginx-templates/post.json b/backend/embed/api_docs/paths/nginx-templates/post.json new file mode 100644 index 000000000..3963d5d58 --- /dev/null +++ b/backend/embed/api_docs/paths/nginx-templates/post.json @@ -0,0 +1,47 @@ +{ + "operationId": "createNginxTemplate", + "summary": "Create a new Nginx Template", + "tags": ["Nginx Templates"], + "requestBody": { + "description": "Template to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateNginxTemplate}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/NginxTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 10, + "created_at": 1646218093000, + "updated_at": 1646218093000, + "user_id": 1, + "name": "My proxy template", + "type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/nginx-templates/templateID/delete.json b/backend/embed/api_docs/paths/nginx-templates/templateID/delete.json new file mode 100644 index 000000000..81e7ff524 --- /dev/null +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/delete.json @@ -0,0 +1,58 @@ +{ + "operationId": "deleteNginxTemplate", + "summary": "Delete a Nginx Template", + "tags": ["Nginx Templates"], + "parameters": [ + { + "in": "path", + "name": "templateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Template", + "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 template that is in use!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/nginx-templates/templateID/get.json b/backend/embed/api_docs/paths/nginx-templates/templateID/get.json new file mode 100644 index 000000000..9cb2097f7 --- /dev/null +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/get.json @@ -0,0 +1,51 @@ +{ + "operationId": "getNginxTemplate", + "summary": "Get a Nginx Template object by ID", + "tags": ["Nginx Templates"], + "parameters": [ + { + "in": "path", + "name": "templateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Host Template", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/NginxTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1646218093000, + "updated_at": 1646218093000, + "user_id": 1, + "name": "Default Proxy Template", + "type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/nginx-templates/templateID/put.json b/backend/embed/api_docs/paths/nginx-templates/templateID/put.json new file mode 100644 index 000000000..84602175b --- /dev/null +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/put.json @@ -0,0 +1,60 @@ +{ + "operationId": "updateNginxTemplate", + "summary": "Update an existing Nginx Template", + "tags": ["Nginx Templates"], + "parameters": [ + { + "in": "path", + "name": "templateID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Template", + "example": 1 + } + ], + "requestBody": { + "description": "Template details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateNginxTemplate}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/NginxTemplateObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1646218093000, + "updated_at": 1646218093000, + "user_id": 1, + "name": "My renamed proxy template", + "type": "proxy", + "template": "# this is a proxy template" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/schema/get.json b/backend/embed/api_docs/paths/schema/get.json new file mode 100644 index 000000000..e21ae8058 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/settings/get.json b/backend/embed/api_docs/paths/settings/get.json new file mode 100644 index 000000000..09b98b8fe --- /dev/null +++ b/backend/embed/api_docs/paths/settings/get.json @@ -0,0 +1,81 @@ +{ + "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": { + "type": "object", + "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_at": 1578010090000, + "updated_at": 1578010095000, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/settings/name/get.json b/backend/embed/api_docs/paths/settings/name/get.json new file mode 100644 index 000000000..38afc7bac --- /dev/null +++ b/backend/embed/api_docs/paths/settings/name/get.json @@ -0,0 +1,52 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_at": 1578010090000, + "updated_at": 1578010095000, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/settings/name/put.json b/backend/embed/api_docs/paths/settings/name/put.json new file mode 100644 index 000000000..f7d5dc780 --- /dev/null +++ b/backend/embed/api_docs/paths/settings/name/put.json @@ -0,0 +1,61 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_at": 1578010090000, + "updated_at": 1578010090000, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/settings/post.json b/backend/embed/api_docs/paths/settings/post.json new file mode 100644 index 000000000..97fa62372 --- /dev/null +++ b/backend/embed/api_docs/paths/settings/post.json @@ -0,0 +1,48 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/SettingObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_at": 1578010090000, + "updated_at": 1578010090000, + "name": "default-site", + "value": { + "html": "

not found

", + "type": "custom" + } + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/streams/get.json b/backend/embed/api_docs/paths/streams/get.json new file mode 100644 index 000000000..9964e47e9 --- /dev/null +++ b/backend/embed/api_docs/paths/streams/get.json @@ -0,0 +1,70 @@ +{ + "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": { + "type": "object", + "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"] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/streams/post.json b/backend/embed/api_docs/paths/streams/post.json new file mode 100644 index 000000000..28f56da6a --- /dev/null +++ b/backend/embed/api_docs/paths/streams/post.json @@ -0,0 +1,39 @@ +{ + "operationId": "createStream", + "summary": "Create a new Stream", + "tags": ["Streams"], + "requestBody": { + "description": "Stream to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateStream}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/streams/streamID/delete.json b/backend/embed/api_docs/paths/streams/streamID/delete.json new file mode 100644 index 000000000..d0f352692 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/streams/streamID/get.json b/backend/embed/api_docs/paths/streams/streamID/get.json new file mode 100644 index 000000000..97a4a76c9 --- /dev/null +++ b/backend/embed/api_docs/paths/streams/streamID/get.json @@ -0,0 +1,43 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/streams/streamID/put.json b/backend/embed/api_docs/paths/streams/streamID/put.json new file mode 100644 index 000000000..787905c5c --- /dev/null +++ b/backend/embed/api_docs/paths/streams/streamID/put.json @@ -0,0 +1,52 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/StreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": "TODO" + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/get.json b/backend/embed/api_docs/paths/upstreams/get.json new file mode 100644 index 000000000..6f96c7092 --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/get.json @@ -0,0 +1,286 @@ +{ + "operationId": "getUpstreams", + "summary": "Get a list of Upstreams", + "tags": ["Upstreams"], + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 5, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_at": 1672804124000, + "updated_at": 1672804124000, + "user_id": 2, + "name": "API servers", + "nginx_template_id": 5, + "ip_hash": true, + "ntlm": false, + "keepalive": 10, + "keepalive_requests": 10, + "keepalive_time": "60s", + "keepalive_timeout": "3s", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 1, + "created_at": 1672804124000, + "updated_at": 1672804124000, + "upstream_group_id": 1, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 2, + "created_at": 1672804124000, + "updated_at": 1672804124000, + "upstream_group_id": 1, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 2, + "created_at": 1672804197000, + "updated_at": 1672804197000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 3, + "created_at": 1672804197000, + "updated_at": 1672804197000, + "upstream_group_id": 2, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 4, + "created_at": 1672804197000, + "updated_at": 1672804197000, + "upstream_group_id": 2, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 3, + "created_at": 1672804200000, + "updated_at": 1672804200000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 5, + "created_at": 1672804200000, + "updated_at": 1672804200000, + "upstream_group_id": 3, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 6, + "created_at": 1672804200000, + "updated_at": 1672804200000, + "upstream_group_id": 3, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 4, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 7, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "upstream_group_id": 4, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 8, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "upstream_group_id": 4, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 5, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 9, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "upstream_group_id": 5, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 10, + "created_at": 1672804201000, + "updated_at": 1672804201000, + "upstream_group_id": 5, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/post.json b/backend/embed/api_docs/paths/upstreams/post.json new file mode 100644 index 000000000..13cdeaac8 --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/post.json @@ -0,0 +1,81 @@ +{ + "operationId": "createUpstream", + "summary": "Create a new Upstream", + "tags": ["Upstreams"], + "requestBody": { + "description": "Upstream to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateUpstream}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 6, + "created_at": 1672806857000, + "updated_at": 1672806857000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ready", + "error_message": "", + "servers": [ + { + "id": 11, + "created_at": 1672806857000, + "updated_at": 1672806857000, + "upstream_id": 6, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 12, + "created_at": 1672806857000, + "updated_at": 1672806857000, + "upstream_id": 6, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json b/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json new file mode 100644 index 000000000..0c54708ff --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json @@ -0,0 +1,58 @@ +{ + "operationId": "deleteUpstream", + "summary": "Delete a Upstream", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Upstream", + "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 Upstream that is in use!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/get.json b/backend/embed/api_docs/paths/upstreams/upstreamID/get.json new file mode 100644 index 000000000..3ad8305ce --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/get.json @@ -0,0 +1,82 @@ +{ + "operationId": "getUpstream", + "summary": "Get a Upstream object by ID", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Upstream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1672786008000, + "updated_at": 1672786008000, + "user_id": 2, + "name": "API servers 3", + "ip_hash": true, + "ntlm": false, + "keepalive": 10, + "keepalive_requests": 10, + "keepalive_time": "60s", + "keepalive_timeout": "3s", + "advanced_config": "", + "servers": [ + { + "id": 1, + "created_at": 1672786009000, + "updated_at": 1672786009000, + "upstream_id": 1, + "server": "api1.localhost:1234", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 2, + "created_at": 1672786009000, + "updated_at": 1672786009000, + "upstream_id": 1, + "server": "api2.localhost:1234", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": true + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/nginx-config/get.json b/backend/embed/api_docs/paths/upstreams/upstreamID/nginx-config/get.json new file mode 100644 index 000000000..f17e4e6db --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/nginx-config/get.json @@ -0,0 +1,43 @@ +{ + "operationId": "getUpstreamNginxConfig", + "summary": "Get a Upstream Nginx Config object by ID", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Upstream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "type": "string" + } + } + }, + "examples": { + "default": { + "value": { + "result": "# ------------------------------------------------------------\n# Upstream 1: API servers\n# ------------------------------------------------------------\nupstream npm_upstream_1 {\nserver 192.168.0.10:80 weight=100 ;\n server 192.168.0.11:80 weight=50 ;\n}\n" + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/put.json b/backend/embed/api_docs/paths/upstreams/upstreamID/put.json new file mode 100644 index 000000000..4df64537f --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/put.json @@ -0,0 +1,94 @@ +{ + "operationId": "updateUpstream", + "summary": "Update an existing Upstream", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Upstream", + "example": 1 + } + ], + "requestBody": { + "description": "Upstream details to update", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.UpdateUpstream}}" + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_at": 1673234177000, + "updated_at": 1673244559000, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ready", + "error_message": "", + "servers": [ + { + "id": 1, + "created_at": 1673234177000, + "updated_at": 1673244559000, + "upstream_id": 1, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 2, + "created_at": 1673234177000, + "updated_at": 1673244559000, + "upstream_id": 1, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/get.json b/backend/embed/api_docs/paths/users/get.json new file mode 100644 index 000000000..4adf79ff8 --- /dev/null +++ b/backend/embed/api_docs/paths/users/get.json @@ -0,0 +1,111 @@ +{ + "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,email.asc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 3, + "offset": 0, + "limit": 100, + "sort": [ + { + "field": "name", + "direction": "ASC" + }, + { + "field": "email", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "name": "Jamie Curnow", + "email": "jc@jc21.com", + "created_at": 1578010090000, + "updated_at": 1578010095000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + }, + { + "id": 2, + "name": "John Doe", + "email": "johdoe@example.com", + "created_at": 1578010100000, + "updated_at": 1578010105000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": [ + "hosts.view", + "hosts.manage" + ] + }, + { + "id": 3, + "name": "Jane Doe", + "email": "janedoe@example.com", + "created_at": 1578010110000, + "updated_at": 1578010115000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": [ + "hosts.view", + "hosts.manage" + ] + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/post.json b/backend/embed/api_docs/paths/users/post.json new file mode 100644 index 000000000..2c4045982 --- /dev/null +++ b/backend/embed/api_docs/paths/users/post.json @@ -0,0 +1,81 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "email": "jc@jc21.com", + "created_at": 1578010100000, + "updated_at": 1578010100000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "auth": { + "$ref": "#/components/schemas/UserAuthObject" + }, + "capabilities": ["full-admin"] + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error"], + "properties": { + "result": { + "type": "object", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/ErrorObject" + } + } + }, + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "An user already exists with this email address" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/auth/post.json b/backend/embed/api_docs/paths/users/userID/auth/post.json new file mode 100644 index 000000000..ce80e960e --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/auth/post.json @@ -0,0 +1,66 @@ +{ + "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 set 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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserAuthObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 2, + "created_at": 1648422222000, + "updated_at": 1648423979000, + "user_id": 3, + "type": "password" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/delete.json b/backend/embed/api_docs/paths/users/userID/delete.json new file mode 100644 index 000000000..0ffa70242 --- /dev/null +++ b/backend/embed/api_docs/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/embed/api_docs/paths/users/userID/get.json b/backend/embed/api_docs/paths/users/userID/get.json new file mode 100644 index 000000000..dab258243 --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/get.json @@ -0,0 +1,60 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "email": "jc@jc21.com", + "created_at": 1578010100000, + "updated_at": 1578010105000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/users/userID/put.json b/backend/embed/api_docs/paths/users/userID/put.json new file mode 100644 index 000000000..1a2b0358c --- /dev/null +++ b/backend/embed/api_docs/paths/users/userID/put.json @@ -0,0 +1,109 @@ +{ + "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": { + "type": "object", + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UserObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "name": "Jamie Curnow", + "email": "jc@jc21.com", + "created_at": 1578010100000, + "updated_at": 1578010110000, + "gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128", + "is_disabled": false, + "capabilities": ["full-admin"] + } + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["error"], + "properties": { + "result": { + "type": "object", + "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!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/main.go b/backend/embed/main.go new file mode 100644 index 000000000..4323d2815 --- /dev/null +++ b/backend/embed/main.go @@ -0,0 +1,23 @@ +package embed + +import "embed" + +// APIDocFiles contain all the files used for swagger schema generation +// +//go:embed api_docs +var APIDocFiles embed.FS + +// Assets are frontend assets served from within this app +// +//go:embed assets +var Assets embed.FS + +// MigrationFiles are database migrations +// +//go:embed migrations +var MigrationFiles embed.FS + +// NginxFiles hold nginx config templates +// +//go:embed nginx +var NginxFiles embed.FS diff --git a/backend/embed/migrations/mysql/20201013035318_initial_schema.sql b/backend/embed/migrations/mysql/20201013035318_initial_schema.sql new file mode 100644 index 000000000..4f2726bcf --- /dev/null +++ b/backend/embed/migrations/mysql/20201013035318_initial_schema.sql @@ -0,0 +1,246 @@ +-- migrate:up + +CREATE TABLE IF NOT EXISTS `jwt_keys` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `public_key` TEXT NOT NULL, + `private_key` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `user` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `name` VARCHAR(50) NOT NULL, + `email` VARCHAR(255) NOT NULL, + `is_system` BOOLEAN NOT NULL DEFAULT FALSE, + `is_disabled` BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS `capability` +( + `name` VARCHAR(50) PRIMARY KEY, + UNIQUE (`name`) +); + +CREATE TABLE IF NOT EXISTS `user_has_capability` +( + `user_id` INT NOT NULL, + `capability_name` VARCHAR(50) NOT NULL, + UNIQUE (`user_id`, `capability_name`), + FOREIGN KEY (`capability_name`) REFERENCES `capability`(`name`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `auth` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `type` VARCHAR(50) NOT NULL, + `identity` VARCHAR(255) NOT NULL, + `secret` VARCHAR(255) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + UNIQUE (`user_id`, `type`) +); + +CREATE TABLE IF NOT EXISTS `setting` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `name` VARCHAR(50) NOT NULL, + `description` VARCHAR(255) NOT NULL DEFAULT '', + `value` TEXT NOT NULL, + UNIQUE (`name`) +); + +CREATE TABLE IF NOT EXISTS `audit_log` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `object_type` VARCHAR(50) NOT NULL, + `object_id` INT NOT NULL, + `action` VARCHAR(50) NOT NULL, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `certificate_authority` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `name` VARCHAR(50) NOT NULL, + `acmesh_server` VARCHAR(255) NOT NULL DEFAULT '', + `ca_bundle` VARCHAR(255) NOT NULL DEFAULT '', + `is_wildcard_supported` BOOLEAN NOT NULL DEFAULT FALSE, -- specific to each CA, acme v1 doesn't usually have wildcards + `max_domains` INT NOT NULL DEFAULT 5, -- per request + `is_readonly` BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS `dns_provider` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `name` VARCHAR(50) NOT NULL, + `acmesh_name` VARCHAR(50) NOT NULL, + `dns_sleep` INT NOT NULL DEFAULT 0, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `certificate` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `type` VARCHAR(50) NOT NULL, -- custom,dns,http + `certificate_authority_id` INT, -- null for a custom cert + `dns_provider_id` INT, -- 0, for a http or custom cert + `name` VARCHAR(50) NOT NULL, + `domain_names` TEXT NOT NULL, + `expires_on` BIGINT NOT NULL DEFAULT 0, + `status` VARCHAR(50) NOT NULL, -- ready,requesting,failed,provided + `error_message` TEXT NOT NULL, + `meta` TEXT NOT NULL, + `is_ecc` BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_authority_id`) REFERENCES `certificate_authority`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`dns_provider_id`) REFERENCES `dns_provider`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `stream` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `listen_interface` VARCHAR(50) NOT NULL, + `incoming_port` INT NOT NULL, + `tcp_forwarding` INT NOT NULL DEFAULT 0, + `udp_forwarding` INT NOT NULL DEFAULT 0, + `advanced_config` TEXT NOT NULL, + `is_disabled` BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `nginx_template` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `name` VARCHAR(50) NOT NULL, + `type` VARCHAR(50) NOT NULL, + `template` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) +); + +CREATE TABLE IF NOT EXISTS `upstream` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `name` VARCHAR(50) NOT NULL, + `nginx_template_id` INT NOT NULL, + `ip_hash` BOOLEAN NOT NULL DEFAULT FALSE, + `ntlm` BOOLEAN NOT NULL DEFAULT FALSE, + `keepalive` INT NOT NULL DEFAULT 0, + `keepalive_requests` INT NOT NULL DEFAULT 0, + `keepalive_time` VARCHAR(50) NOT NULL DEFAULT '', + `keepalive_timeout` VARCHAR(50) NOT NULL DEFAULT '', + `advanced_config` TEXT NOT NULL, + `status` VARCHAR(50) NOT NULL DEFAULT '', + `error_message` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `upstream_server` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `upstream_id` INT NOT NULL, + `server` VARCHAR(50) NOT NULL, + `weight` INT NOT NULL DEFAULT 0, + `max_conns` INT NOT NULL DEFAULT 0, + `max_fails` INT NOT NULL DEFAULT 0, + `fail_timeout` INT NOT NULL DEFAULT 0, + `is_backup` BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `access_list` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `name` VARCHAR(50) NOT NULL, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `host` +( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + `is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism + `user_id` INT NOT NULL, + `type` VARCHAR(50) NOT NULL, + `nginx_template_id` INT NOT NULL, + `listen_interface` VARCHAR(50) NOT NULL DEFAULT '', + `domain_names` TEXT NOT NULL, + `upstream_id` INT, + `proxy_scheme` VARCHAR(50) NOT NULL DEFAULT '', + `proxy_host` VARCHAR(50) NOT NULL DEFAULT '', + `proxy_port` INT NOT NULL DEFAULT 0, + `certificate_id` INT, + `access_list_id` INT, + `ssl_forced` BOOLEAN NOT NULL DEFAULT FALSE, + `caching_enabled` BOOLEAN NOT NULL DEFAULT FALSE, + `block_exploits` BOOLEAN NOT NULL DEFAULT FALSE, + `allow_websocket_upgrade` BOOLEAN NOT NULL DEFAULT FALSE, + `http2_support` BOOLEAN NOT NULL DEFAULT FALSE, + `hsts_enabled` BOOLEAN NOT NULL DEFAULT FALSE, + `hsts_subdomains` BOOLEAN NOT NULL DEFAULT FALSE, + `paths` TEXT NOT NULL, + `advanced_config` TEXT NOT NULL, + `status` VARCHAR(50) NOT NULL DEFAULT '', + `error_message` TEXT NOT NULL, + `is_disabled` BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_id`) REFERENCES `certificate`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`access_list_id`) REFERENCES `access_list`(`id`) ON DELETE CASCADE +); + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/embed/migrations/mysql/20201013035839_initial_data.sql b/backend/embed/migrations/mysql/20201013035839_initial_data.sql new file mode 100644 index 000000000..9f24a6a0b --- /dev/null +++ b/backend/embed/migrations/mysql/20201013035839_initial_data.sql @@ -0,0 +1,337 @@ +-- migrate:up + +-- User permissions +INSERT INTO `capability` ( + `name` +) VALUES + ("full-admin"), + ("access-lists.view"), + ("access-lists.manage"), + ("audit-log.view"), + ("certificates.view"), + ("certificates.manage"), + ("certificate-authorities.view"), + ("certificate-authorities.manage"), + ("dns-providers.view"), + ("dns-providers.manage"), + ("hosts.view"), + ("hosts.manage"), + ("nginx-templates.view"), + ("nginx-templates.manage"), + ("settings.manage"), + ("streams.view"), + ("streams.manage"), + ("users.manage"); + +INSERT INTO `setting` ( + `created_at`, + `updated_at`, + `name`, + `description`, + `value` +) VALUES +-- Default site +( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "default-site", + "What to show users who hit your Nginx server by default", + '"welcome"' -- remember this is json +), +( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "auth-methods", + "Which methods are enabled for authentication", + '["local"]' -- remember this is json +), +( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "oauth-auth", + "Configuration for OAuth authentication", + '{}' -- remember this is json +), +( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "ldap-auth", + "Configuration for LDAP authentication", + '{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json +); + +-- Default Certificate Authorities + +INSERT INTO `certificate_authority` ( + `created_at`, + `updated_at`, + `name`, + `acmesh_server`, + `is_wildcard_supported`, + `max_domains`, + `is_readonly` +) VALUES ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "ZeroSSL", + "zerossl", + TRUE, + 10, + TRUE +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "Let's Encrypt", + "https://acme-v02.api.letsencrypt.org/directory", + TRUE, + 10, + TRUE +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "Buypass Go SSL", + "https://api.buypass.com/acme/directory", + FALSE, + 5, + TRUE +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "SSL.com", + "ssl.com", + FALSE, + 10, + TRUE +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "Let's Encrypt (Testing)", + "https://acme-staging-v02.api.letsencrypt.org/directory", + TRUE, + 10, + TRUE +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "Buypass Go SSL (Testing)", + "https://api.test4.buypass.no/acme/directory", + FALSE, + 5, + TRUE +); + +-- System User +INSERT INTO `user` ( + `created_at`, + `updated_at`, + `name`, + `email`, + `is_system` +) VALUES ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + "System", + "system@localhost", + TRUE +); + +-- Host Templates +INSERT INTO `nginx_template` ( + `created_at`, + `updated_at`, + `user_id`, + `name`, + `type`, + `template` +) VALUES ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + (SELECT `id` FROM `user` WHERE `is_system` IS TRUE LIMIT 1), + "Default Proxy Template", + "proxy", + "# ------------------------------------------------------------ +{{#each Host.DomainNames}} +# {{this}} +{{/each}} +# ------------------------------------------------------------ + +server { + {{#if Config.Ipv4}} + listen 80; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:80; + {{/if}} + + {{#if Certificate.ID}} + {{#if Config.Ipv4}} + listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{/if}} + + server_name {{#each Host.DomainNames}}{{this}} {{/each}}; + + {{#if Certificate.ID}} + include conf.d/include/ssl-ciphers.conf; + {{#if Certificate.IsAcme}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{else}} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; + {{/if}} + {{/if}} + + {{#if Host.CachingEnabled}} + include conf.d/include/assets.conf; + {{/if}} + + {{#if Host.BlockExploits}} + include conf.d/include/block-exploits.conf; + {{/if}} + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + # Force SSL + include conf.d/include/force-ssl.conf; + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log /data/logs/host-{{Host.ID}}_access.log proxy; + error_log /data/logs/host-{{Host.ID}}_error.log warn; + + {{Host.AdvancedConfig}} + + # locations ? + + # default location: + location / { + {{#if Host.AccessListID}} + # Authorization + auth_basic ""Authorization required""; + auth_basic_user_file /data/access/{{Host.AccessListID}}; + # access_list.passauth ? todo + {{/if}} + + # Access Rules ? todo + + # Access checks must...? todo + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + {{/if}} + + # Proxy! + 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_http_version 1.1; + + {{#if Upstream.ID}} + # upstream + proxy_pass {{Host.ProxyScheme}}://npm_upstream_{{Upstream.ID}}; + {{else}} + # proxy a single host + proxy_pass {{Host.ProxyScheme}}://{{Host.ProxyHost}}:{{Host.ProxyPort}}; + {{/if}} + } + + # Legacy Custom Configuration + include /data/nginx/custom/server_proxy[.]conf; +} +" +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + (SELECT `id` FROM `user` WHERE `is_system` IS TRUE LIMIT 1), + "Default Redirect Template", + "redirect", + "# this is a redirect template" +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + (SELECT `id` FROM `user` WHERE `is_system` IS TRUE LIMIT 1), + "Default Dead Template", + "dead", + "# this is a dead template" +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + (SELECT `id` FROM `user` WHERE `is_system` IS TRUE LIMIT 1), + "Default Stream Template", + "stream", + "# this is a stream template" +), ( + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), + (SELECT `id` FROM `user` WHERE `is_system` IS TRUE LIMIT 1), + "Default Upstream Template", + "upstream", + "# ------------------------------------------------------------ +# Upstream {{Upstream.ID}}: {{Upstream.Name}} +# ------------------------------------------------------------ + +upstream npm_upstream_{{Upstream.ID}} { + + {{#if Upstream.IPHash~}} + ip_hash; + {{~/if}} + + {{#if Upstream.NTLM~}} + ntlm; + {{~/if}} + + {{#if Upstream.Keepalive~}} + keepalive {{Upstream.Keepalive}}; + {{~/if}} + + {{#if Upstream.KeepaliveRequests~}} + keepalive_requests {{Upstream.KeepaliveRequests}}; + {{~/if}} + + {{#if Upstream.KeepaliveTime~}} + keepalive_time {{Upstream.KeepaliveTime}}; + {{~/if}} + + {{#if Upstream.KeepaliveTimeout~}} + keepalive_timeout {{Upstream.KeepaliveTimeout}}; + {{~/if}} + + {{Upstream.AdvancedConfig}} + + {{#each Upstream.Servers~}} + {{#unless IsDeleted~}} + server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; + {{/unless}} + {{/each}} +} +" +); + +-- migrate:down diff --git a/backend/embed/migrations/postgres/20201013035318_initial_schema.sql b/backend/embed/migrations/postgres/20201013035318_initial_schema.sql new file mode 100644 index 000000000..fa75dd7de --- /dev/null +++ b/backend/embed/migrations/postgres/20201013035318_initial_schema.sql @@ -0,0 +1,212 @@ +-- migrate:up + +CREATE TABLE "jwt_keys" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "public_key" TEXT NOT NULL, + "private_key" TEXT NOT NULL +); + +CREATE TABLE "user" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "name" VARCHAR(50) NOT NULL, + "email" VARCHAR(255) NOT NULL, + "is_system" BOOLEAN NOT NULL DEFAULT FALSE, + "is_disabled" BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE "capability" ( + "name" TEXT NOT NULL PRIMARY KEY, + UNIQUE ("name") +); + +CREATE TABLE "user_has_capability" ( + "user_id" INTEGER NOT NULL, + "capability_name" TEXT NOT NULL REFERENCES "capability"("name") ON DELETE CASCADE, + UNIQUE ("user_id", "capability_name") +); + +CREATE TABLE "auth" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "type" VARCHAR(50) NOT NULL, + "identity" VARCHAR(255) NOT NULL, + "secret" VARCHAR(255) NOT NULL, + UNIQUE ("user_id", "type") +); + +CREATE TABLE "setting" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "name" VARCHAR(50) NOT NULL, + "description" VARCHAR(255) NOT NULL DEFAULT '', + "value" TEXT NOT NULL, + UNIQUE ("name") +); + +CREATE TABLE "audit_log" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "object_type" VARCHAR(50) NOT NULL, + "object_id" INTEGER NOT NULL, + "action" VARCHAR(50) NOT NULL, + "meta" TEXT NOT NULL +); + +CREATE TABLE "certificate_authority" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "name" VARCHAR(50) NOT NULL, + "acmesh_server" VARCHAR(255) NOT NULL DEFAULT '', + "ca_bundle" VARCHAR(255) NOT NULL DEFAULT '', + "is_wildcard_supported" BOOLEAN NOT NULL DEFAULT FALSE, -- specific to each CA, acme v1 doesn't usually have wildcards + "max_domains" INTEGER NOT NULL DEFAULT 5, -- per request + "is_readonly" BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE "dns_provider" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "name" VARCHAR(50) NOT NULL, + "acmesh_name" VARCHAR(50) NOT NULL, + "dns_sleep" INTEGER NOT NULL DEFAULT 0, + "meta" TEXT NOT NULL +); + +CREATE TABLE "certificate" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "type" VARCHAR(50) NOT NULL, -- custom,dns,http + "certificate_authority_id" INTEGER REFERENCES "certificate_authority"("id") ON DELETE CASCADE, -- 0 for a custom cert + "dns_provider_id" INTEGER REFERENCES "dns_provider"("id") ON DELETE CASCADE, -- 0, for a http or custom cert + "name" VARCHAR(50) NOT NULL, + "domain_names" TEXT NOT NULL, + "expires_on" BIGINT NOT NULL DEFAULT 0, + "status" VARCHAR(50) NOT NULL, -- ready,requesting,failed,provided + "error_message" TEXT NOT NULL DEFAULT '', + "meta" TEXT NOT NULL, + "is_ecc" BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE "stream" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "listen_interface" VARCHAR(50) NOT NULL, + "incoming_port" INTEGER NOT NULL, + "tcp_forwarding" INTEGER NOT NULL DEFAULT 0, + "udp_forwarding" INTEGER NOT NULL DEFAULT 0, + "advanced_config" TEXT NOT NULL, + "is_disabled" BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE "nginx_template" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "name" VARCHAR(50) NOT NULL, + "type" VARCHAR(50) NOT NULL, + "template" TEXT NOT NULL +); + +CREATE TABLE "upstream" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "name" VARCHAR(50) NOT NULL, + "nginx_template_id" INTEGER NOT NULL REFERENCES "nginx_template"("id") ON DELETE CASCADE, + "ip_hash" BOOLEAN NOT NULL DEFAULT FALSE, + "ntlm" BOOLEAN NOT NULL DEFAULT FALSE, + "keepalive" INTEGER NOT NULL DEFAULT 0, + "keepalive_requests" INTEGER NOT NULL DEFAULT 0, + "keepalive_time" VARCHAR(50) NOT NULL DEFAULT '', + "keepalive_timeout" VARCHAR(50) NOT NULL DEFAULT '', + "advanced_config" TEXT NOT NULL, + "status" VARCHAR(50) NOT NULL DEFAULT '', + "error_message" TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE "upstream_server" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "upstream_id" INTEGER NOT NULL REFERENCES "upstream"("id") ON DELETE CASCADE, + "server" VARCHAR(50) NOT NULL, + "weight" INTEGER NOT NULL DEFAULT 0, + "max_conns" INTEGER NOT NULL DEFAULT 0, + "max_fails" INTEGER NOT NULL DEFAULT 0, + "fail_timeout" INTEGER NOT NULL DEFAULT 0, + "is_backup" BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE TABLE "access_list" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "name" VARCHAR(50) NOT NULL, + "meta" TEXT NOT NULL +); + +CREATE TABLE "host" ( + "id" SERIAL PRIMARY KEY, + "created_at" BIGINT NOT NULL DEFAULT 0, + "updated_at" BIGINT NOT NULL DEFAULT 0, + "is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism + "user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, + "type" TEXT NOT NULL, + "nginx_template_id" INTEGER NOT NULL REFERENCES "nginx_template"("id") ON DELETE CASCADE, + "listen_interface" TEXT NOT NULL DEFAULT '', + "domain_names" TEXT NOT NULL, + "upstream_id" INTEGER REFERENCES "upstream"("id") ON DELETE CASCADE, + "proxy_scheme" TEXT NOT NULL DEFAULT '', + "proxy_host" TEXT NOT NULL DEFAULT '', + "proxy_port" INTEGER NOT NULL DEFAULT 0, + "certificate_id" INTEGER REFERENCES "certificate"("id") ON DELETE CASCADE, + "access_list_id" INTEGER REFERENCES "access_list"("id") ON DELETE CASCADE, + "ssl_forced" BOOLEAN NOT NULL DEFAULT FALSE, + "caching_enabled" BOOLEAN NOT NULL DEFAULT FALSE, + "block_exploits" BOOLEAN NOT NULL DEFAULT FALSE, + "allow_websocket_upgrade" BOOLEAN NOT NULL DEFAULT FALSE, + "http2_support" BOOLEAN NOT NULL DEFAULT FALSE, + "hsts_enabled" BOOLEAN NOT NULL DEFAULT FALSE, + "hsts_subdomains" BOOLEAN NOT NULL DEFAULT FALSE, + "paths" TEXT NOT NULL DEFAULT '', + "advanced_config" TEXT NOT NULL DEFAULT '', + "status" TEXT NOT NULL DEFAULT '', + "error_message" TEXT NOT NULL DEFAULT '', + "is_disabled" BOOLEAN NOT NULL DEFAULT FALSE +); + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/embed/migrations/postgres/20201013035839_initial_data.sql b/backend/embed/migrations/postgres/20201013035839_initial_data.sql new file mode 100644 index 000000000..0937851af --- /dev/null +++ b/backend/embed/migrations/postgres/20201013035839_initial_data.sql @@ -0,0 +1,337 @@ +-- migrate:up + +-- User permissions +INSERT INTO "capability" ( + "name" +) VALUES + ('full-admin'), + ('access-lists.view'), + ('access-lists.manage'), + ('audit-log.view'), + ('certificates.view'), + ('certificates.manage'), + ('certificate-authorities.view'), + ('certificate-authorities.manage'), + ('dns-providers.view'), + ('dns-providers.manage'), + ('hosts.view'), + ('hosts.manage'), + ('nginx-templates.view'), + ('nginx-templates.manage'), + ('settings.manage'), + ('streams.view'), + ('streams.manage'), + ('users.manage'); + +INSERT INTO "setting" ( + "created_at", + "updated_at", + "name", + "description", + "value" +) VALUES +-- Default site +( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'default-site', + 'What to show users who hit your Nginx server by default', + '"welcome"' -- remember this is json +), +( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'auth-methods', + 'Which methods are enabled for authentication', + '["local"]' -- remember this is json +), +( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'oauth-auth', + 'Configuration for OAuth authentication', + '{}' -- remember this is json +), +( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'ldap-auth', + 'Configuration for LDAP authentication', + '{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json +); + +-- Default Certificate Authorities + +INSERT INTO "certificate_authority" ( + "created_at", + "updated_at", + "name", + "acmesh_server", + "is_wildcard_supported", + "max_domains", + "is_readonly" +) VALUES ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'ZeroSSL', + 'zerossl', + TRUE, + 10, + TRUE +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'Let''s Encrypt', + 'https://acme-v02.api.letsencrypt.org/directory', + TRUE, + 10, + TRUE +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'Buypass Go SSL', + 'https://api.buypass.com/acme/directory', + FALSE, + 5, + TRUE +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'SSL.com', + 'ssl.com', + FALSE, + 10, + TRUE +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'Let''s Encrypt (Testing)', + 'https://acme-staging-v02.api.letsencrypt.org/directory', + TRUE, + 10, + TRUE +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'Buypass Go SSL (Testing)', + 'https://api.test4.buypass.no/acme/directory', + FALSE, + 5, + TRUE +); + +-- System User +INSERT INTO "user" ( + "created_at", + "updated_at", + "name", + "email", + "is_system" +) VALUES ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + 'System', + 'system@localhost', + TRUE +); + +-- Host Templates +INSERT INTO "nginx_template" ( + "created_at", + "updated_at", + "user_id", + "name", + "type", + "template" +) VALUES ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + (SELECT "id" FROM "user" WHERE "is_system" IS TRUE LIMIT 1), + 'Default Proxy Template', + 'proxy', + '# ------------------------------------------------------------ +{{#each Host.DomainNames}} +# {{this}} +{{/each}} +# ------------------------------------------------------------ + +server { + {{#if Config.Ipv4}} + listen 80; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:80; + {{/if}} + + {{#if Certificate.ID}} + {{#if Config.Ipv4}} + listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{/if}} + + server_name {{#each Host.DomainNames}}{{this}} {{/each}}; + + {{#if Certificate.ID}} + include conf.d/include/ssl-ciphers.conf; + {{#if Certificate.IsAcme}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{else}} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; + {{/if}} + {{/if}} + + {{#if Host.CachingEnabled}} + include conf.d/include/assets.conf; + {{/if}} + + {{#if Host.BlockExploits}} + include conf.d/include/block-exploits.conf; + {{/if}} + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + # Force SSL + include conf.d/include/force-ssl.conf; + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log /data/logs/host-{{Host.ID}}_access.log proxy; + error_log /data/logs/host-{{Host.ID}}_error.log warn; + + {{Host.AdvancedConfig}} + + # locations ? + + # default location: + location / { + {{#if Host.AccessListID}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file /data/access/{{Host.AccessListID}}; + # access_list.passauth ? todo + {{/if}} + + # Access Rules ? todo + + # Access checks must...? todo + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + {{/if}} + + # Proxy! + 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_http_version 1.1; + + {{#if Upstream.ID}} + # upstream + proxy_pass {{Host.ProxyScheme}}://npm_upstream_{{Upstream.ID}}; + {{else}} + # proxy a single host + proxy_pass {{Host.ProxyScheme}}://{{Host.ProxyHost}}:{{Host.ProxyPort}}; + {{/if}} + } + + # Legacy Custom Configuration + include /data/nginx/custom/server_proxy[.]conf; +} +' +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + (SELECT "id" FROM "user" WHERE "is_system" IS TRUE LIMIT 1), + 'Default Redirect Template', + 'redirect', + '# this is a redirect template' +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + (SELECT "id" FROM "user" WHERE "is_system" IS TRUE LIMIT 1), + 'Default Dead Template', + 'dead', + '# this is a dead template' +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + (SELECT "id" FROM "user" WHERE "is_system" IS TRUE LIMIT 1), + 'Default Stream Template', + 'stream', + '# this is a stream template' +), ( + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, + (SELECT "id" FROM "user" WHERE "is_system" IS TRUE LIMIT 1), + 'Default Upstream Template', + 'upstream', + '# ------------------------------------------------------------ +# Upstream {{Upstream.ID}}: {{Upstream.Name}} +# ------------------------------------------------------------ + +upstream npm_upstream_{{Upstream.ID}} { + + {{#if Upstream.IPHash~}} + ip_hash; + {{~/if}} + + {{#if Upstream.NTLM~}} + ntlm; + {{~/if}} + + {{#if Upstream.Keepalive~}} + keepalive {{Upstream.Keepalive}}; + {{~/if}} + + {{#if Upstream.KeepaliveRequests~}} + keepalive_requests {{Upstream.KeepaliveRequests}}; + {{~/if}} + + {{#if Upstream.KeepaliveTime~}} + keepalive_time {{Upstream.KeepaliveTime}}; + {{~/if}} + + {{#if Upstream.KeepaliveTimeout~}} + keepalive_timeout {{Upstream.KeepaliveTimeout}}; + {{~/if}} + + {{Upstream.AdvancedConfig}} + + {{#each Upstream.Servers~}} + {{#unless IsDeleted~}} + server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; + {{/unless}} + {{/each}} +} +' +); + +-- migrate:down diff --git a/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql b/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql new file mode 100644 index 000000000..602be0323 --- /dev/null +++ b/backend/embed/migrations/sqlite/20201013035318_initial_schema.sql @@ -0,0 +1,246 @@ +-- migrate:up + +CREATE TABLE IF NOT EXISTS `jwt_keys` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `public_key` TEXT NOT NULL, + `private_key` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `user` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `email` TEXT NOT NULL, + `is_system` INTEGER NOT NULL DEFAULT 0, + `is_disabled` INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `capability` +( + `name` TEXT PRIMARY KEY, + UNIQUE (`name`) +); + +CREATE TABLE IF NOT EXISTS `user_has_capability` +( + `user_id` INTEGER NOT NULL, + `capability_name` TEXT NOT NULL, + UNIQUE (`user_id`, `capability_name`), + FOREIGN KEY (`capability_name`) REFERENCES `capability` (`name`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `auth` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, + `identity` TEXT NOT NULL, + `secret` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + UNIQUE (`user_id`, `type`) +); + +CREATE TABLE IF NOT EXISTS `setting` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `description` TEXT NOT NULL DEFAULT "", + `value` TEXT NOT NULL, + UNIQUE (`name`) +); + +CREATE TABLE IF NOT EXISTS `audit_log` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` 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`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `certificate_authority` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `name` TEXT NOT NULL, + `acmesh_server` TEXT NOT NULL DEFAULT "", + `ca_bundle` TEXT NOT NULL DEFAULT "", + `is_wildcard_supported` INTEGER NOT NULL DEFAULT 0, -- specific to each CA, acme v1 doesn't usually have wildcards + `max_domains` INTEGER NOT NULL DEFAULT 5, -- per request + `is_readonly` INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS `dns_provider` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `acmesh_name` TEXT NOT NULL, + `dns_sleep` INTEGER NOT NULL DEFAULT 0, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `certificate` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, -- custom,dns,http + `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 NOT NULL DEFAULT 0, + `status` TEXT NOT NULL, -- ready,requesting,failed,provided + `error_message` TEXT NOT NULL DEFAULT "", + `meta` TEXT NOT NULL, + `is_ecc` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_authority_id`) REFERENCES `certificate_authority` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`dns_provider_id`) REFERENCES `dns_provider` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `stream` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `listen_interface` TEXT NOT NULL, + `incoming_port` INTEGER 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, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `nginx_template` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `type` TEXT NOT NULL, + `template` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `upstream` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `nginx_template_id` INTEGER NOT NULL, + `ip_hash` INTEGER NOT NULL DEFAULT 0, + `ntlm` INTEGER NOT NULL DEFAULT 0, + `keepalive` INTEGER NOT NULL DEFAULT 0, + `keepalive_requests` INTEGER NOT NULL DEFAULT 0, + `keepalive_time` TEXT NOT NULL DEFAULT "", + `keepalive_timeout` TEXT NOT NULL DEFAULT "", + `advanced_config` TEXT NOT NULL, + `status` TEXT NOT NULL DEFAULT "", + `error_message` TEXT NOT NULL DEFAULT "", + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `upstream_server` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `upstream_id` INTEGER NOT NULL, + `server` TEXT NOT NULL, + `weight` INTEGER NOT NULL DEFAULT 0, + `max_conns` INTEGER NOT NULL DEFAULT 0, + `max_fails` INTEGER NOT NULL DEFAULT 0, + `fail_timeout` INTEGER NOT NULL DEFAULT 0, + `is_backup` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `access_list` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `name` TEXT NOT NULL, + `meta` TEXT NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `host` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT, + `created_at` INTEGER NOT NULL DEFAULT 0, + `updated_at` INTEGER NOT NULL DEFAULT 0, + `is_deleted` INTEGER NOT NULL DEFAULT 0, + `user_id` INTEGER NOT NULL, + `type` TEXT NOT NULL, + `nginx_template_id` INTEGER NOT NULL, + `listen_interface` TEXT NOT NULL DEFAULT "", + `domain_names` TEXT NOT NULL, + `upstream_id` INTEGER, + `proxy_scheme` TEXT NOT NULL DEFAULT "", + `proxy_host` TEXT NOT NULL DEFAULT "", + `proxy_port` INTEGER NOT NULL DEFAULT 0, + `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 DEFAULT "", + `advanced_config` TEXT NOT NULL DEFAULT "", + `status` TEXT NOT NULL DEFAULT "", + `error_message` TEXT NOT NULL DEFAULT "", + `is_disabled` INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`nginx_template_id`) REFERENCES `nginx_template` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`upstream_id`) REFERENCES `upstream` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`certificate_id`) REFERENCES `certificate` (`id`) ON DELETE CASCADE, + FOREIGN KEY (`access_list_id`) REFERENCES `access_list` (`id`) ON DELETE CASCADE +); + +-- migrate:down + +-- Not allowed to go down from initial diff --git a/backend/embed/migrations/sqlite/20201013035839_initial_data.sql b/backend/embed/migrations/sqlite/20201013035839_initial_data.sql new file mode 100644 index 000000000..5bfe40161 --- /dev/null +++ b/backend/embed/migrations/sqlite/20201013035839_initial_data.sql @@ -0,0 +1,336 @@ +-- migrate:up + +-- User permissions +INSERT INTO `capability` ( + name +) VALUES + ("full-admin"), + ("access-lists.view"), + ("access-lists.manage"), + ("audit-log.view"), + ("certificates.view"), + ("certificates.manage"), + ("certificate-authorities.view"), + ("certificate-authorities.manage"), + ("dns-providers.view"), + ("dns-providers.manage"), + ("hosts.view"), + ("hosts.manage"), + ("nginx-templates.view"), + ("nginx-templates.manage"), + ("settings.manage"), + ("streams.view"), + ("streams.manage"), + ("users.manage"); + +-- Default site +INSERT INTO `setting` ( + created_at, + updated_at, + name, + description, + value +) VALUES ( + unixepoch() * 1000, + unixepoch() * 1000, + "default-site", + "What to show users who hit your Nginx server by default", + '"welcome"' -- remember this is json +), +( + unixepoch() * 1000, + unixepoch() * 1000, + "auth-methods", + "Which methods are enabled for authentication", + '["local"]' -- remember this is json +), +( + unixepoch() * 1000, + unixepoch() * 1000, + "oauth-auth", + "Configuration for OAuth authentication", + '{}' -- remember this is json +), +( + unixepoch() * 1000, + unixepoch() * 1000, + "ldap-auth", + "Configuration for LDAP authentication", + '{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json +); + +-- Default Certificate Authorities + +INSERT INTO `certificate_authority` ( + created_at, + updated_at, + name, + acmesh_server, + is_wildcard_supported, + max_domains, + is_readonly +) VALUES ( + unixepoch() * 1000, + unixepoch() * 1000, + "ZeroSSL", + "zerossl", + 1, + 10, + 1 +), ( + unixepoch() * 1000, + unixepoch() * 1000, + "Let's Encrypt", + "https://acme-v02.api.letsencrypt.org/directory", + 1, + 10, + 1 +), ( + unixepoch() * 1000, + unixepoch() * 1000, + "Buypass Go SSL", + "https://api.buypass.com/acme/directory", + 0, + 5, + 1 +), ( + unixepoch() * 1000, + unixepoch() * 1000, + "SSL.com", + "ssl.com", + 0, + 10, + 1 +), ( + unixepoch() * 1000, + unixepoch() * 1000, + "Let's Encrypt (Testing)", + "https://acme-staging-v02.api.letsencrypt.org/directory", + 1, + 10, + 1 +), ( + unixepoch() * 1000, + unixepoch() * 1000, + "Buypass Go SSL (Testing)", + "https://api.test4.buypass.no/acme/directory", + 0, + 5, + 1 +); + +-- System User +INSERT INTO `user` ( + created_at, + updated_at, + name, + email, + is_system +) VALUES ( + unixepoch() * 1000, + unixepoch() * 1000, + "System", + "system@localhost", + 1 +); + +-- Host Templates +INSERT INTO `nginx_template` ( + created_at, + updated_at, + user_id, + name, + type, + template +) VALUES ( + unixepoch() * 1000, + unixepoch() * 1000, + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Proxy Template", + "proxy", + "# ------------------------------------------------------------ +{{#each Host.DomainNames}} +# {{this}} +{{/each}} +# ------------------------------------------------------------ + +server { + {{#if Config.Ipv4}} + listen 80; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:80; + {{/if}} + + {{#if Certificate.ID}} + {{#if Config.Ipv4}} + listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{/if}} + + server_name {{#each Host.DomainNames}}{{this}} {{/each}}; + + {{#if Certificate.ID}} + include conf.d/include/ssl-ciphers.conf; + {{#if Certificate.IsAcme}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{else}} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; + {{/if}} + {{/if}} + + {{#if Host.CachingEnabled}} + include conf.d/include/assets.conf; + {{/if}} + + {{#if Host.BlockExploits}} + include conf.d/include/block-exploits.conf; + {{/if}} + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + # Force SSL + include conf.d/include/force-ssl.conf; + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log /data/logs/host-{{Host.ID}}_access.log proxy; + error_log /data/logs/host-{{Host.ID}}_error.log warn; + + {{Host.AdvancedConfig}} + + # locations ? + + # default location: + location / { + {{#if Host.AccessListID}} + # Authorization + auth_basic ""Authorization required""; + auth_basic_user_file /data/access/{{Host.AccessListID}}; + # access_list.passauth ? todo + {{/if}} + + # Access Rules ? todo + + # Access checks must...? todo + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + {{/if}} + + # Proxy! + 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_http_version 1.1; + + {{#if Upstream.ID}} + # upstream + proxy_pass {{Host.ProxyScheme}}://npm_upstream_{{Upstream.ID}}; + {{else}} + # proxy a single host + proxy_pass {{Host.ProxyScheme}}://{{Host.ProxyHost}}:{{Host.ProxyPort}}; + {{/if}} + } + + # Legacy Custom Configuration + include /data/nginx/custom/server_proxy[.]conf; +} +" +), ( + unixepoch() * 1000, + unixepoch() * 1000, + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Redirect Template", + "redirect", + "# this is a redirect template" +), ( + unixepoch() * 1000, + unixepoch() * 1000, + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Dead Template", + "dead", + "# this is a dead template" +), ( + unixepoch() * 1000, + unixepoch() * 1000, + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Stream Template", + "stream", + "# this is a stream template" +), ( + unixepoch() * 1000, + unixepoch() * 1000, + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Upstream Template", + "upstream", + "# ------------------------------------------------------------ +# Upstream {{Upstream.ID}}: {{Upstream.Name}} +# ------------------------------------------------------------ + +upstream npm_upstream_{{Upstream.ID}} { + + {{#if Upstream.IPHash~}} + ip_hash; + {{~/if}} + + {{#if Upstream.NTLM~}} + ntlm; + {{~/if}} + + {{#if Upstream.Keepalive~}} + keepalive {{Upstream.Keepalive}}; + {{~/if}} + + {{#if Upstream.KeepaliveRequests~}} + keepalive_requests {{Upstream.KeepaliveRequests}}; + {{~/if}} + + {{#if Upstream.KeepaliveTime~}} + keepalive_time {{Upstream.KeepaliveTime}}; + {{~/if}} + + {{#if Upstream.KeepaliveTimeout~}} + keepalive_timeout {{Upstream.KeepaliveTimeout}}; + {{~/if}} + + {{Upstream.AdvancedConfig}} + + {{#each Upstream.Servers~}} + {{#unless IsDeleted~}} + server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; + {{/unless}} + {{/each}} +} +" +); + +-- migrate:down diff --git a/backend/embed/nginx/_assets.conf.hbs b/backend/embed/nginx/_assets.conf.hbs new file mode 100644 index 000000000..736397673 --- /dev/null +++ b/backend/embed/nginx/_assets.conf.hbs @@ -0,0 +1,4 @@ +{{#if caching_enabled}} + # Asset Caching + include conf.d/include/assets.conf; +{{/if}} diff --git a/backend/embed/nginx/_certificates.conf.hbs b/backend/embed/nginx/_certificates.conf.hbs new file mode 100644 index 000000000..d114f9820 --- /dev/null +++ b/backend/embed/nginx/_certificates.conf.hbs @@ -0,0 +1,13 @@ +{{#if certificate}} + {{#if (equal certificate.certificate_authority_id "0")}} + # Custom SSL + ssl_certificate {{npm_data_dir}}/custom_ssl/npm-{{certificate.id}}/fullchain.pem; + ssl_certificate_key {{npm_data_dir}}/custom_ssl/npm-{{certificate.id}}/privkey.pem; + {{else}} + # Acme SSL + include {{nginx_conf_dir}}/npm/conf.d/acme-challenge.conf; + include {{nginx_conf_dir}}/npm/conf.d/include/ssl-ciphers.conf; + ssl_certificate {{acme_certs_dir}}/npm-{{certificate.id}}/fullchain.pem; + ssl_certificate_key {{acme_certs_dir}}/npm-{{certificate.id}}/privkey.pem; + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_forced_ssl.conf.hbs b/backend/embed/nginx/_forced_ssl.conf.hbs new file mode 100644 index 000000000..970296e03 --- /dev/null +++ b/backend/embed/nginx/_forced_ssl.conf.hbs @@ -0,0 +1,6 @@ +{{#if certificate}} + {{#if ssl_forced}} + # Force SSL + include {{nginx_conf_dir}}/npm/conf.d/include/force-ssl.conf; + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_hsts.conf.hbs b/backend/embed/nginx/_hsts.conf.hbs new file mode 100644 index 000000000..c27da5aa3 --- /dev/null +++ b/backend/embed/nginx/_hsts.conf.hbs @@ -0,0 +1,8 @@ +{{#if certificate}} + {{#if ssl_forced}} + {{#if hsts_enabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security "max-age=63072000;{{#if hsts_subdomains}} includeSubDomains;{{/if}} preload" always; + {{/if}} + {{/if}} +{{/if}} diff --git a/backend/embed/nginx/_listen.conf.hbs b/backend/embed/nginx/_listen.conf.hbs new file mode 100644 index 000000000..217da00f9 --- /dev/null +++ b/backend/embed/nginx/_listen.conf.hbs @@ -0,0 +1,18 @@ +listen 80; + +{{#if ipv6}} + listen [::]:80; +{{else}} + #listen [::]:80; +{{/if}} + +{{#if certificate}} + listen 443 ssl{% if http2_support %} http2{% endif %}; + {{#if ipv6}} + listen [::]:443; + {{else}} + #listen [::]:443; + {{/if}} +{{/if}} + +server_name{{#each domain_names}} {{this}}{{/each}}; diff --git a/backend/embed/nginx/_location.conf.hbs b/backend/embed/nginx/_location.conf.hbs new file mode 100644 index 000000000..167d46eb8 --- /dev/null +++ b/backend/embed/nginx/_location.conf.hbs @@ -0,0 +1,40 @@ +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}} + {{#if access_list.items}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file {{npm_data_dir}}/access/{{access_list.id}}; + {{access_list.passauth}} + {{/if}} + + # Access Rules + {{#each access_list.clients as |client clientIdx|}} + {{client.rule}}; + {{/each}}deny all; + + # Access checks must... + {{#if access_list.satisfy}} + {{access_list.satisfy}}; + {{/if}} + {{/if}} + + {{> inc_assets}} + {{> inc_forced_ssl}} + {{> inc_hsts}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + {{advanced_config}} + } + diff --git a/backend/embed/nginx/acme-request.conf.hbs b/backend/embed/nginx/acme-request.conf.hbs new file mode 100644 index 000000000..aac148959 --- /dev/null +++ b/backend/embed/nginx/acme-request.conf.hbs @@ -0,0 +1,15 @@ +server { + listen 80; + {{#if ipv6}} + listen [::]:80; + {{/if}} + + server_name{{#each domain_names}} {{this}}{{/each}}; + access_log {{npm_data_dir}}/logs/acme-requests_access.log standard; + error_log {{npm_data_dir}}/logs/acme-requests_error.log warn; + {{nginx_conf_dir}}/npm/conf.d/include/letsencrypt-acme-challenge.conf; + + location / { + return 404; + } +} diff --git a/backend/embed/nginx/dead_host.conf.hbs b/backend/embed/nginx/dead_host.conf.hbs new file mode 100644 index 000000000..289940c89 --- /dev/null +++ b/backend/embed/nginx/dead_host.conf.hbs @@ -0,0 +1,20 @@ +{{#if enabled}} + server { + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + access_log {{npm_data_dir}}/logs/dead-host-{{id}}_access.log standard; + error_log {{npm_data_dir}}/logs/dead-host-{{id}}_error.log warn; + + {{advanced_config}} + + {{#if use_default_location}} + location / { + {{> inc_hsts}} + return 404; + } + {{/if}} + } +{{/if}} diff --git a/backend/embed/nginx/default.conf.hbs b/backend/embed/nginx/default.conf.hbs new file mode 100644 index 000000000..190ec02bb --- /dev/null +++ b/backend/embed/nginx/default.conf.hbs @@ -0,0 +1,35 @@ +{{#if (equal value "congratulations")}} + # Skipping output, congratulations page configration is baked in. +{{else}} + server { + listen 80 default; + {{#if ipv6}} + listen [::]:80; + {{else}} + #listen [::]:80; + {{/if}} + + server_name default-host.localhost; + access_log {{npm_data_dir}}/logs/default-host_access.log combined; + error_log {{npm_data_dir}}/logs/default-host_error.log warn; + + {{#if (equal value "404")}} + location / { + return 404; + } + {{/if}} + + {{#if (equal value "redirect")}} + location / { + return 301 {{meta.redirect}}; + } + {{/if}} + + {{#if (equal value "html")}} + root {{npm_data_dir}}/nginx/default_www; + location / { + try_files $uri /index.html; + } + {{/if}} + } +{{/if}} diff --git a/backend/embed/nginx/ip_ranges.conf.hbs b/backend/embed/nginx/ip_ranges.conf.hbs new file mode 100644 index 000000000..7b7c3d07e --- /dev/null +++ b/backend/embed/nginx/ip_ranges.conf.hbs @@ -0,0 +1,3 @@ +{{#each ip_ranges as |range rangeIdx|}} + set_real_ip_from {{range}}; +{{/each}} diff --git a/backend/embed/nginx/proxy_host.conf.hbs b/backend/embed/nginx/proxy_host.conf.hbs new file mode 100644 index 000000000..e7681f4ba --- /dev/null +++ b/backend/embed/nginx/proxy_host.conf.hbs @@ -0,0 +1,62 @@ +{{#if enabled}} + server { + set $forward_scheme {{forward_scheme}}; + set $server "{{forward_host}}"; + set $port {{forward_port}}; + + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_assets}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log {{npm_data_dir}}/logs/proxy-host-{{id}}_access.log proxy; + error_log {{npm_data_dir}}/logs/proxy-host-{{id}}_error.log warn; + + {{advanced_config}} + {{locations}} + + {{#if use_default_location}} + location / { + {{#if access_list}} + {{#if access_list.items}} + # Authorization + auth_basic "Authorization required"; + auth_basic_user_file {{npm_data_dir}}/access/{{access_list.id}}; + {{access_list.passauth}} + {{/if}} + + # Access Rules + {{#each access_list.clients as |client clientIdx|}} + {{client.rule}}; + {{/each}}deny all; + + # Access checks must... + {{#if access_list.satisfy}} + {{access_list.satisfy}}; + {{/if}} + {{/if}} + + {{> inc_hsts}} + + {{#if allow_websocket_upgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + # Proxy! + include {{nginx_conf_dir}}/npm/conf.d/include/proxy.conf; + } + {{/if}} + + # Custom + include {{npm_data_dir}}/nginx/custom/server_proxy[.]conf; + } +{{/if}} diff --git a/backend/embed/nginx/redirection_host.conf.hbs b/backend/embed/nginx/redirection_host.conf.hbs new file mode 100644 index 000000000..18208c6c3 --- /dev/null +++ b/backend/embed/nginx/redirection_host.conf.hbs @@ -0,0 +1,28 @@ +{{#if enabled}} + server { + {{> inc_listen}} + {{> inc_certificates}} + {{> inc_assets}} + {{> inc_hsts}} + {{> inc_forced_ssl}} + + access_log {{npm_data_dir}}/logs/redirection-host-{{ id }}_access.log standard; + error_log {{npm_data_dir}}/logs/redirection-host-{{ id }}_error.log warn; + + {{advanced_config}} + + {{#if use_default_location}} + location / { + {{> inc_hsts}} + {{#if preserve_path}} + return {{forward_http_code}} {{forward_scheme}}://{{forward_domain_name}}$request_uri; + {{else}} + return {{forward_http_code}} {{forward_scheme}}://{{forward_domain_name}}; + {{/if}} + } + {{/if}} + + # Custom + include {{npm_data_dir}}/nginx/custom/server_redirect[.]conf; + } +{{/if}} diff --git a/backend/embed/nginx/stream.conf.hbs b/backend/embed/nginx/stream.conf.hbs new file mode 100644 index 000000000..bc85bbfa4 --- /dev/null +++ b/backend/embed/nginx/stream.conf.hbs @@ -0,0 +1,34 @@ +{{#if enabled}} + {{#if tcp_forwarding}} + server { + listen {{incoming_port}}; + {{#if ipv6}} + listen [::]:{{incoming_port}}; + {{else}} + #listen [::]:{{incoming_port}}; + {{/if}} + + proxy_pass {{forward_ip}}:{{forwarding_port}}; + + # Custom + include {{npm_data_dir}}/nginx/custom/server_stream[.]conf; + include {{npm_data_dir}}/nginx/custom/server_stream_tcp[.]conf; + } + {{/if}} + + {{#if udp_forwarding}} + server { + listen {{incoming_port}} udp; + {{#if ipv6}} + listen [::]:{{ incoming_port }} udp; + {{else}} + #listen [::]:{{incoming_port}} udp; + {{/if}} + proxy_pass {{forward_ip}}:{{forwarding_port}}; + + # Custom + include {{npm_data_dir}}/nginx/custom/server_stream[.]conf; + include {{npm_data_dir}}/nginx/custom/server_stream_udp[.]conf; + } + {{/if}} +{{/if}} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 000000000..43b54830a --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,98 @@ +module npm + +go 1.24 + +toolchain go1.24.3 + +require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/alexflint/go-arg v1.5.1 + github.com/amacneil/dbmate/v2 v2.20.0 + github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e + github.com/fatih/color v1.18.0 + github.com/glebarez/sqlite v1.11.0 + github.com/go-chi/chi/v5 v5.2.2 + github.com/go-chi/cors v1.2.2 + github.com/go-chi/jwtauth/v5 v5.3.3 + github.com/go-ldap/ldap/v3 v3.4.11 + github.com/jc21/go-sse v1.7.0 + github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/qri-io/jsonschema v0.2.1 + github.com/rotisserie/eris v0.5.4 + github.com/stretchr/testify v1.10.0 + github.com/vrischmann/envconfig v1.4.1 + go.uber.org/automaxprocs v1.6.0 + go.uber.org/goleak v1.3.0 + golang.org/x/crypto v0.39.0 + golang.org/x/oauth2 v0.30.0 + gorm.io/datatypes v1.2.6 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.30.0 + gorm.io/plugin/soft_delete v1.2.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.6 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c // indirect + github.com/lestrrat-go/jwx/v2 v2.1.3 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lestrrat-go/pdebug/v3 v3.0.1 // indirect + github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/qri-io/jsonpointer v0.1.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/uint128 v1.3.0 // indirect + modernc.org/cc/v3 v3.41.0 // indirect + modernc.org/ccgo/v3 v3.17.0 // indirect + modernc.org/libc v1.61.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/sqlite v1.28.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) + +replace github.com/amacneil/dbmate/v2 => github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1 + +replace modernc.org/sqlite => gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 000000000..bb7796ce9 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,261 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= +github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= +github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= +github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible h1:Ppm0npCCsmuR9oQaBtRuZcmILVE74aXE+AmrJj8L2ns= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +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/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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo= +github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1 h1:WEZwsDG5eXdgh0NfA9SpuShOP6rJCah22ihvZsaoimM= +github.com/jc21/dbmate/v2 v2.0.0-20230527023241-0aaa124cc0f1/go.mod h1:XAPcHsokw7nyvFbuN9FdcYb8JSEUTaJDFYOirnNxEvc= +github.com/jc21/go-sse v1.7.0 h1:Lavb7FGssS2UdJMK1P3r4rQJgg2JZx7BINi8pd/QzKg= +github.com/jc21/go-sse v1.7.0/go.mod h1:dbA0LtDgvSEhlAXz+meN1KAcdvcCLiF0aVdhjhZjbGI= +github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96 h1:LzBXyNdALQHC7DUy8PE7IFTihmnSRop4Ps2cBIr/WP8= +github.com/jc21/jsref v0.0.0-20250501111625-0ce4620b7d96/go.mod h1:Vno1sw0yqqImXnYy3q6ueIG+h+Vwwfbkfw9DTHCrHfY= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +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/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= +github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +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/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo= +github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo= +github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/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-20210312050401-7f8bd69d6acb h1:DDg5u5lk2v8O8qxs8ecQkMUBj3tLW6wkSLzxxOyi1Ig= +github.com/lestrrat-go/structinfo v0.0.0-20210312050401-7f8bd69d6acb/go.mod h1:i+E8Uf04vf2QjOWyJdGY75vmG+4rxiZW2kIj1lTB5mo= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= +github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vrischmann/envconfig v1.4.1 h1:fucz2HsoAkJCLgIngWdWqLNxNjdWD14zfrLF6EQPdY4= +github.com/vrischmann/envconfig v1.4.1/go.mod h1:cX3p+/PEssil6fWwzIS7kf8iFpli3giuxXGHxckucYc= +github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= +github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= +gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38= +gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck= +gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.1.3/go.mod h1:AKDgRWk8lcSQSw+9kxCJnX/yySj8G3rdwYlU57cB45c= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/plugin/soft_delete v1.2.1 h1:qx9D/c4Xu6w5KT8LviX8DgLcB9hkKl6JC9f44Tj7cGU= +gorm.io/plugin/soft_delete v1.2.1/go.mod h1:Zv7vQctOJTGOsJ/bWgrN1n3od0GBAZgnLjEx+cApLGk= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= +modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE= +modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index 8d42d0969..000000000 --- a/backend/index.js +++ /dev/null @@ -1,135 +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; - let envSqliteFile = process.env.DB_SQLITE_FILE || null; - - 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) && !envSqliteFile){ - envSqliteFile = '/data/database.sqlite'; - logger.info(`No valid environment variables for database provided, using default SQLite file '${envSqliteFile}'`); - } - - 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 knex 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 knex configuration'); - 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.debug('Wrote db configuration to config file: ' + filename); - 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 083bfa62e..000000000 --- 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(internalNginx.reload) - .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/acme/acmesh.go b/backend/internal/acme/acmesh.go new file mode 100644 index 000000000..a7e57d0b1 --- /dev/null +++ b/backend/internal/acme/acmesh.go @@ -0,0 +1,210 @@ +package acme + +// Some light reading: +// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "npm/internal/config" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/logger" + + "github.com/rotisserie/eris" +) + +func getAcmeShFilePath() (string, error) { + path, err := exec.LookPath("acme.sh") + if err != nil { + return path, eris.Wrapf(err, "Cannot find acme.sh execuatable script in PATH") + } + return path, nil +} + +func getCommonEnvVars() []string { + return []string{ + fmt.Sprintf("ACMESH_CONFIG_HOME=%s", os.Getenv("ACMESH_CONFIG_HOME")), + fmt.Sprintf("ACMESH_HOME=%s", os.Getenv("ACMESH_HOME")), + fmt.Sprintf("CERT_HOME=%s", os.Getenv("CERT_HOME")), + fmt.Sprintf("LE_CONFIG_HOME=%s", os.Getenv("LE_CONFIG_HOME")), + fmt.Sprintf("LE_WORKING_DIR=%s", os.Getenv("LE_WORKING_DIR")), + } +} + +// GetAcmeShVersion will return the acme.sh script version +func GetAcmeShVersion() string { + if r, err := shExec([]string{"--version"}, nil); err == nil { + // modify the output + r = strings.Trim(r, "\n") + v := strings.Split(r, "\n") + return v[len(v)-1] + } + return "" +} + +// CreateAccountKey is required for each server initially +func CreateAccountKey(ca *certificateauthority.Model) error { + args := []string{"--create-account-key", "--accountkeylength", "2048"} + if ca != nil { + logger.Info("Acme.sh CreateAccountKey for %s", ca.AcmeshServer) + args = append(args, "--server", ca.AcmeshServer) + if ca.CABundle != "" { + args = append(args, "--ca-bundle", ca.CABundle) + } + } else { + logger.Info("Acme.sh CreateAccountKey") + } + + args = append(args, getCommonArgs()...) + ret, err := shExec(args, nil) + if err != nil { + return err + } + + logger.Debug("CreateAccountKey returned:\n%+v", ret) + + return nil +} + +// RequestCert does all the heavy lifting +func RequestCert(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) (string, error) { + args, err := buildCertRequestArgs(domains, method, outputFullchainFile, outputKeyFile, dnsProvider, ca, force) + if err != nil { + return err.Error(), err + } + + envs := make([]string, 0) + if dnsProvider != nil { + envs, err = dnsProvider.GetAcmeShEnvVars() + if err != nil { + return err.Error(), err + } + } + + ret, err := shExec(args, envs) + if err != nil { + return ret, err + } + + return "", nil +} + +// shExec executes the acme.sh with arguments +func shExec(args []string, envs []string) (string, error) { + acmeSh, err := getAcmeShFilePath() + if err != nil { + logger.Error("AcmeShError", err) + return "", err + } + + logger.Debug("CMD: %s %v", acmeSh, args) + // nolint: gosec + c := exec.Command(acmeSh, args...) + c.Env = append(getCommonEnvVars(), envs...) + + b, e := c.CombinedOutput() + + if e != nil { + // logger.Error("AcmeShError", eris.Wrapf(e, "Command error: %s -- %v\n%+v", acmeSh, args, e)) + logger.Warn(string(b)) + } + + return string(b), e +} + +func getCommonArgs() []string { + args := make([]string, 0) + + if config.Configuration.Acmesh.Home != "" { + args = append(args, "--home", config.Configuration.Acmesh.Home) + } + if config.Configuration.Acmesh.ConfigHome != "" { + args = append(args, "--config-home", config.Configuration.Acmesh.ConfigHome) + } + if config.Configuration.Acmesh.CertHome != "" { + args = append(args, "--cert-home", config.Configuration.Acmesh.CertHome) + } + + args = append(args, "--log", "/data/logs/acme.sh.log") + args = append(args, "--debug", "2") + + return args +} + +// This is split out into it's own function so it's testable +func buildCertRequestArgs( + domains []string, + method, + outputFullchainFile, + outputKeyFile string, + dnsProvider *dnsprovider.Model, + ca *certificateauthority.Model, + force bool, +) ([]string, error) { + // The argument order matters. + // see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode + // for multiple domains and note that the method of validation is required just after the domain arg, each time. + + // TODO log file location configurable + args := []string{"--issue"} + + if ca != nil { + args = append(args, "--server", ca.AcmeshServer) + if ca.CABundle != "" { + args = append(args, "--ca-bundle", ca.CABundle) + } + } + + if outputFullchainFile != "" { + args = append(args, "--fullchain-file", outputFullchainFile) + } + + if outputKeyFile != "" { + args = append(args, "--key-file", outputKeyFile) + } + + methodArgs := make([]string, 0) + switch method { + case "dns": + if dnsProvider == nil { + return nil, ErrDNSNeedsDNSProvider + } + methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeshName) + if dnsProvider.DNSSleep > 0 { + // See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck + methodArgs = append(methodArgs, "--dnssleep", fmt.Sprintf("%d", dnsProvider.DNSSleep)) + } + + case "http": + if dnsProvider != nil { + return nil, ErrHTTPHasDNSProvider + } + methodArgs = append(methodArgs, "-w", config.Configuration.Acmesh.GetWellknown()) + default: + return nil, ErrMethodNotSupported + } + + hasMethod := false + + // Add domains to args + for _, domain := range domains { + args = append(args, "-d", domain) + // Method has to appear after each domain + if !hasMethod { + args = append(args, methodArgs...) + hasMethod = true + } + } + + if force { + args = append(args, "--force") + } + + args = append(args, getCommonArgs()...) + + return args, nil +} diff --git a/backend/internal/acme/acmesh_test.go b/backend/internal/acme/acmesh_test.go new file mode 100644 index 000000000..267327055 --- /dev/null +++ b/backend/internal/acme/acmesh_test.go @@ -0,0 +1,252 @@ +package acme + +import ( + "fmt" + "testing" + + "npm/internal/config" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +// TODO configurable +const acmeLogFile = "/data/logs/acme.sh.log" + +func TestBuildCertRequestArgs(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type want struct { + args []string + err error + } + + wellknown := config.Configuration.Acmesh.GetWellknown() + exampleKey := fmt.Sprintf("%s/example.com.key", config.Configuration.Acmesh.CertHome) + exampleChain := fmt.Sprintf("%s/a.crt", config.Configuration.Acmesh.CertHome) + + tests := []struct { + name string + domains []string + method string + outputFullchainFile string + outputKeyFile string + dnsProvider *dnsprovider.Model + ca *certificateauthority.Model + want want + }{ + { + name: "http single domain", + domains: []string{"example.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "-w", + wellknown, + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "http multiple domains", + domains: []string{"example.com", "example-two.com", "example-three.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "-w", + wellknown, + "-d", + "example-two.com", + "-d", + "example-three.com", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "http single domain with dns provider", + domains: []string{"example.com"}, + method: "http", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: nil, + err: ErrHTTPHasDNSProvider, + }, + }, + { + name: "dns single domain", + domains: []string{"example.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "--dns", + "dns_cf", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "dns multiple domains", + domains: []string{"example.com", "example-two.com", "example-three.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: &dnsprovider.Model{ + AcmeshName: "dns_cf", + }, + ca: nil, + want: want{ + args: []string{ + "--issue", + "--fullchain-file", + exampleChain, + "--key-file", + exampleKey, + "-d", + "example.com", + "--dns", + "dns_cf", + "-d", + "example-two.com", + "-d", + "example-three.com", + "--log", + acmeLogFile, + "--debug", + "2", + }, + err: nil, + }, + }, + { + name: "dns single domain no provider", + domains: []string{"example.com"}, + method: "dns", + outputFullchainFile: exampleChain, + outputKeyFile: exampleKey, + dnsProvider: nil, + ca: nil, + want: want{ + args: nil, + err: ErrDNSNeedsDNSProvider, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, err := buildCertRequestArgs(tt.domains, tt.method, tt.outputFullchainFile, tt.outputKeyFile, tt.dnsProvider, tt.ca, false) + + assert.Equal(t, tt.want.args, args) + assert.Equal(t, tt.want.err, err) + }) + } +} + +func TestGetAcmeShFilePath(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + path, err := getAcmeShFilePath() + if err != nil { + assert.Equal(t, "Cannot find acme.sh execuatable script in PATH: exec: \"acme.sh\": executable file not found in $PATH", err.Error()) + assert.Equal(t, "", path) + } else { + assert.Equal(t, "/bin/acme.sh", path) + } + }) +} + +func TestGetCommonEnvVars(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + t.Setenv("ACMESH_CONFIG_HOME", "/data/.acme.sh/config") + t.Setenv("ACMESH_HOME", "/data/.acme.sh") + t.Setenv("CERT_HOME", "/data/.acme.sh/certs") + t.Setenv("LE_CONFIG_HOME", "/data/.acme.sh/config") + t.Setenv("LE_WORKING_DIR", "/data/.acme.sh") + + expected := []string{ + "ACMESH_CONFIG_HOME=/data/.acme.sh/config", + "ACMESH_HOME=/data/.acme.sh", + "CERT_HOME=/data/.acme.sh/certs", + "LE_CONFIG_HOME=/data/.acme.sh/config", + "LE_WORKING_DIR=/data/.acme.sh", + } + vals := getCommonEnvVars() + assert.Equal(t, expected, vals) + }) +} + +func TestGetAcmeShVersion(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + resp := GetAcmeShVersion() + // Seems like a pointless test, however when this is run in CI + // it doesn't have access to the acme.sh command so it will + // always be empty. But when running in Docker, it will. + if resp != "" { + assert.Equal(t, "v", resp[:1]) + } + }) +} diff --git a/backend/internal/acme/errors.go b/backend/internal/acme/errors.go new file mode 100644 index 000000000..d68850ff2 --- /dev/null +++ b/backend/internal/acme/errors.go @@ -0,0 +1,12 @@ +package acme + +import ( + "github.com/rotisserie/eris" +) + +// All errors relating to Acme.sh use +var ( + ErrDNSNeedsDNSProvider = eris.New("RequestCert dns method requires a dns provider") + ErrHTTPHasDNSProvider = eris.New("RequestCert http method does not need a dns provider") + ErrMethodNotSupported = eris.New("RequestCert method not supported") +) diff --git a/backend/internal/api/context/context.go b/backend/internal/api/context/context.go new file mode 100644 index 000000000..dc9f32b93 --- /dev/null +++ b/backend/internal/api/context/context.go @@ -0,0 +1,27 @@ +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"} + // SortCtxKey is the name of the Sort value on the context + SortCtxKey = &contextKey{"Sort"} + // 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/context/context_test.go b/backend/internal/api/context/context_test.go new file mode 100644 index 000000000..9eee4c669 --- /dev/null +++ b/backend/internal/api/context/context_test.go @@ -0,0 +1,17 @@ +package context + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetString(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + assert.Equal(t, "context value: Body", BodyCtxKey.String()) + }) +} diff --git a/backend/internal/api/handler/access_lists.go b/backend/internal/api/handler/access_lists.go new file mode 100644 index 000000000..6a8335d13 --- /dev/null +++ b/backend/internal/api/handler/access_lists.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/accesslist" +) + +// GetAccessLists will return a list of Access Lists +// Route: GET /access-lists +func GetAccessLists() 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 := accesslist.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) + } + } +} + +// GetAccessList will return a single access list +// Route: GET /access-lists/{accessListID} +func GetAccessList() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var accessListID uint + if accessListID, err = getURLParamInt(r, "accessListID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := accesslist.GetByID(accessListID) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// CreateAccessList will create an access list +// Route: POST /access-lists +func CreateAccessList() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newItem accesslist.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).(uint) + newItem.UserID = userID + + if err = newItem.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Access List: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newItem) + } +} + +// UpdateAccessList is self explanatory +// Route: PUT /access-lists/{accessListID} +func UpdateAccessList() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var accessListID uint + if accessListID, err = getURLParamInt(r, "accessListID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := accesslist.GetByID(accessListID) + 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) + } + } +} + +// DeleteAccessList is self explanatory +// Route: DELETE /access-lists/{accessListID} +func DeleteAccessList() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var accessListID uint + if accessListID, err = getURLParamInt(r, "accessListID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := accesslist.GetByID(accessListID) + 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/auth.go b/backend/internal/api/handler/auth.go new file mode 100644 index 000000000..54f428c21 --- /dev/null +++ b/backend/internal/api/handler/auth.go @@ -0,0 +1,240 @@ +package handler + +import ( + "encoding/json" + "net/http" + "slices" + "time" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/entity/auth" + "npm/internal/entity/setting" + "npm/internal/entity/user" + "npm/internal/errors" + njwt "npm/internal/jwt" + "npm/internal/logger" + + "gorm.io/gorm" +) + +// 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"` +} + +// GetAuthConfig is anonymous and returns the types of authentication +// enabled for this site +func GetAuthConfig() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + val, err := setting.GetAuthMethods() + if err == gorm.ErrRecordNotFound { + h.ResultResponseJSON(w, r, http.StatusOK, nil) + return + } else if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, val) + } +} + +// NewToken Also known as a Login, requesting a new token with credentials +// Route: POST /auth +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 + } + + // Check that this auth type is enabled + if authMethods, err := setting.GetAuthMethods(); err == gorm.ErrRecordNotFound { + h.ResultResponseJSON(w, r, http.StatusOK, nil) + return + } else if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } else if !slices.Contains(authMethods, payload.Type) { + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidAuthType.Error(), nil) + return + } + + switch payload.Type { + case "ldap": + newTokenLDAP(w, r, payload) + case "local": + newTokenLocal(w, r, payload) + } + } +} + +func newTokenLocal(w http.ResponseWriter, r *http.Request, payload tokenPayload) { + // Find user by email + userObj, userErr := user.GetByEmail(payload.Identity) + if userErr != nil { + logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), userErr.Error()) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + if userObj.IsDisabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) + return + } + + // Get Auth + authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type) + if authErr != nil { + logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error()) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + // Verify Auth + validateErr := authObj.ValidateSecret(payload.Secret) + if validateErr != nil { + logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error()) + // Sleep for 1 second to prevent brute force password guessing + time.Sleep(time.Second) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + if response, err := njwt.Generate(&userObj, false); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } +} + +func newTokenLDAP(w http.ResponseWriter, r *http.Request, payload tokenPayload) { + // Get LDAP settings + ldapSettings, err := setting.GetLDAPSettings() + if err != nil { + logger.Error("LDAP settings not found", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + + // Lets try to authenticate with LDAP + ldapUser, err := auth.LDAPAuthenticate(payload.Identity, payload.Secret) + if err != nil { + logger.Error("LDAP Auth Error", err) + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + // Get Auth by identity + authObj, authErr := auth.GetByIdenityType(ldapUser.Username, payload.Type) + if authErr == gorm.ErrRecordNotFound { + // Auth is not found for this identity. We can create it + if !ldapSettings.AutoCreateUser { + // LDAP Login was successful, but user does not have an auth record + // and auto create is disabled. Showing account disabled error + // for the time being + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrUserDisabled.Error(), nil) + return + } + + // Attempt to find user by email + foundUser, err := user.GetByEmail(ldapUser.Email) + if err == gorm.ErrRecordNotFound { + // User not found, create user + foundUser, err = user.CreateFromLDAPUser(ldapUser) + if err != nil { + logger.Error("user.CreateFromLDAPUser", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + logger.Info("Created user from LDAP: %s, %s", ldapUser.Username, foundUser.Email) + } else if err != nil { + logger.Error("user.GetByEmail", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + + // Create auth record and attach to this user + authObj = auth.Model{ + UserID: foundUser.ID, + Type: auth.TypeLDAP, + Identity: ldapUser.Username, + } + if err := authObj.Save(); err != nil { + logger.Error("auth.Save", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + logger.Info("Created LDAP auth for user: %s, %s", ldapUser.Username, foundUser.Email) + } else if authErr != nil { + logger.Error("auth.GetByIdenityType", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, authErr.Error(), nil) + return + } + + userObj, userErr := user.GetByID(authObj.UserID) + if userErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil) + return + } + + if userObj.IsDisabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) + return + } + + if response, err := njwt.Generate(&userObj, false); 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: POST /auth/refresh +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, false); err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, response) + } + } +} + +// NewSSEToken will generate and return a very short lived token for +// use by the /sse/* endpoint. It requires an app token to generate this +// Route: POST /auth/sse +func NewSSEToken() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + userID := r.Context().Value(c.UserIDCtxKey).(uint) + + // Find user + userObj, userErr := user.GetByID(userID) + if userErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) + return + } + + if userObj.IsDisabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) + return + } + + if response, err := njwt.Generate(&userObj, true); 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/certificate_authorities.go b/backend/internal/api/handler/certificate_authorities.go new file mode 100644 index 000000000..cdd9e5257 --- /dev/null +++ b/backend/internal/api/handler/certificate_authorities.go @@ -0,0 +1,152 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + "npm/internal/acme" + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/certificateauthority" + "npm/internal/logger" + + "gorm.io/gorm" +) + +// 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 uint + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := certificateauthority.GetByID(caID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// 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.Check(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.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 + } + + if err = acme.CreateAccountKey(&newCA); err != nil { + logger.Error("CreateAccountKeyError", err) + } + + h.ResultResponseJSON(w, r, http.StatusOK, newCA) + } +} + +// UpdateCertificateAuthority updates a ca +// 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 uint + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + ca, err := certificateauthority.GetByID(caID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + 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.Check(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.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) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteCertificateAuthority deletes a ca +// 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 uint + if caID, err = getURLParamInt(r, "caID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := certificateauthority.GetByID(caID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} diff --git a/backend/internal/api/handler/certificates.go b/backend/internal/api/handler/certificates.go new file mode 100644 index 000000000..618d6d4fc --- /dev/null +++ b/backend/internal/api/handler/certificates.go @@ -0,0 +1,187 @@ +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" + "npm/internal/entity/host" + "npm/internal/jobqueue" + "npm/internal/logger" + + "gorm.io/gorm" +) + +// 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), middleware.GetExpandFromContext(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) { + if item := getCertificateFromRequest(w, r); item != nil { + // nolint: errcheck,gosec + item.Expand(middleware.GetExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// CreateCertificate will create a Certificate +// Route: POST /certificates +func CreateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var item certificate.Model + if fillObjectFromBody(w, r, "", &item) { + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) + item.UserID = userID + + if err := item.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil) + return + } + + configureCertificate(item) + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// UpdateCertificate updates a cert +// Route: PUT /certificates/{certificateID} +func UpdateCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if item := getCertificateFromRequest(w, r); item != nil { + // 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. + if fillObjectFromBody(w, r, schema.UpdateCertificate(item.Type), item) { + if err := item.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + // configureCertificate(*item, item.Request) + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } + } +} + +// DeleteCertificate deletes a cert +// Route: DELETE /certificates/{certificateID} +func DeleteCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if item := getCertificateFromRequest(w, r); item != nil { + cnt := host.GetCertificateUseCount(item.ID) + if cnt > 0 { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot delete certificate that is in use by at least 1 host", nil) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + } + } +} + +// RenewCertificate is self explanatory +// Route: PUT /certificates/{certificateID}/renew +func RenewCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if item := getCertificateFromRequest(w, r); item != nil { + configureCertificate(*item) + h.ResultResponseJSON(w, r, http.StatusOK, true) + } + } +} + +// DownloadCertificate is self explanatory +// Route: PUT /certificates/{certificateID}/download +func DownloadCertificate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if item := getCertificateFromRequest(w, r); item != nil { + // todo + h.ResultResponseJSON(w, r, http.StatusOK, "ok") + } + } +} + +// getCertificateFromRequest has some reusable code for all endpoints that +// have a certificate id in the url. it will write errors to the output. +func getCertificateFromRequest(w http.ResponseWriter, r *http.Request) *certificate.Model { + var err error + var certificateID uint + if certificateID, err = getURLParamInt(r, "certificateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return nil + } + + certificateObject, err := certificate.GetByID(certificateID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + return &certificateObject + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + return nil +} + +// fillObjectFromBody has some reusable code for all endpoints that +// have a certificate id in the url. it will write errors to the output. +func fillObjectFromBody(w http.ResponseWriter, r *http.Request, validationSchema string, o any) bool { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + if validationSchema != "" { + schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), validationSchema, bodyBytes) + if jsonErr != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil) + return false + } + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return false + } + } + + err := json.Unmarshal(bodyBytes, o) + if err != nil { + logger.Debug("Unmarshal Error: %+v", err) + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return false + } + + return true +} + +func configureCertificate(cert certificate.Model) { + err := jobqueue.AddJob(jobqueue.Job{ + Name: "RequestCertificate", + Action: cert.Request, + }) + if err != nil { + logger.Error("ConfigureCertificateError", err) + } +} diff --git a/backend/internal/api/handler/config.go b/backend/internal/api/handler/config.go new file mode 100644 index 000000000..3f6bc9544 --- /dev/null +++ b/backend/internal/api/handler/config.go @@ -0,0 +1,16 @@ +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 000000000..517d14bee --- /dev/null +++ b/backend/internal/api/handler/dns_providers.go @@ -0,0 +1,173 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/dnsproviders" + "npm/internal/entity/dnsprovider" + "npm/internal/errors" + + "gorm.io/gorm" +) + +// 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 uint + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// 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).(uint) + 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 updates a provider +// 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 uint + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + 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) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteDNSProvider removes a provider +// 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 uint + if providerID, err = getURLParamInt(r, "providerID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := dnsprovider.GetByID(providerID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// GetAcmeshProviders will return a list of acme.sh providers +// Route: GET /dns-providers/acmesh +func GetAcmeshProviders() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + h.ResultResponseJSON(w, r, http.StatusOK, dnsproviders.List()) + } +} + +// GetAcmeshProvider will return a single acme.sh provider +// Route: GET /dns-providers/acmesh/{acmeshID} +func GetAcmeshProvider() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var acmeshID string + var err error + if acmeshID, err = getURLParamString(r, "acmeshID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, getErr := dnsproviders.Get(acmeshID) + switch getErr { + case errors.ErrProviderNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, getErr.Error(), nil) + } + } +} diff --git a/backend/internal/api/handler/health.go b/backend/internal/api/handler/health.go new file mode 100644 index 000000000..6f0ac3137 --- /dev/null +++ b/backend/internal/api/handler/health.go @@ -0,0 +1,33 @@ +package handler + +import ( + "net/http" + + "npm/internal/acme" + h "npm/internal/api/http" + "npm/internal/config" +) + +type healthCheckResponse struct { + Version string `json:"version"` + Commit string `json:"commit"` + AcmeShVersion string `json:"acme.sh"` + Healthy bool `json:"healthy"` + IsSetup bool `json:"setup"` +} + +// 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, + AcmeShVersion: acme.GetAcmeShVersion(), + } + + 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 000000000..f04b42253 --- /dev/null +++ b/backend/internal/api/handler/helpers.go @@ -0,0 +1,104 @@ +package handler + +import ( + "net/http" + "strconv" + + "npm/internal/model" + + "github.com/go-chi/chi/v5" + "github.com/rotisserie/eris" +) + +const defaultLimit = 10 + +func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) { + pageInfo := model.PageInfo{} + var err error + + pageInfo.Offset, pageInfo.Limit, err = getPagination(r) + if err != nil { + return pageInfo, err + } + + return pageInfo, nil +} + +func getQueryVarString(r *http.Request, varName string, required bool, defaultValue string) (string, error) { + queryValues := r.URL.Query() + varValue := queryValues.Get(varName) + + if varValue == "" && required { + return "", eris.Errorf("%v was not supplied in the request", varName) + } else if varValue == "" { + return defaultValue, nil + } + + return varValue, nil +} + +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, eris.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, eris.Wrapf(intErr, "%v is not a valid number", varName) + } + + return varInt, nil +} + +func getURLParamInt(r *http.Request, varName string) (uint, error) { + var defaultValue uint + + required := true + paramStr := chi.URLParam(r, varName) + + if paramStr == "" && required { + return 0, eris.Errorf("%v was not supplied in the request", varName) + } else if paramStr == "" { + return defaultValue, nil + } + + paramUint, err := strconv.ParseUint(paramStr, 10, 32) + if err != nil { + return 0, eris.Wrapf(err, "%v is not a valid number", varName) + } + + return uint(paramUint), nil +} + +func getURLParamString(r *http.Request, varName string) (string, error) { + required := true + defaultValue := "" + paramStr := chi.URLParam(r, varName) + + if paramStr == "" && required { + return "", eris.Errorf("%v was not supplied in the request", varName) + } else if paramStr == "" { + return defaultValue, nil + } + + return paramStr, 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/helpers_test.go b/backend/internal/api/handler/helpers_test.go new file mode 100644 index 000000000..c8c1fd6c6 --- /dev/null +++ b/backend/internal/api/handler/helpers_test.go @@ -0,0 +1,119 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "npm/internal/model" + + "github.com/stretchr/testify/assert" +) + +func TestGetPageInfoFromRequest(t *testing.T) { + t.Run("basic test", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/hosts", nil) + p, err := getPageInfoFromRequest(r) + + var nilStringSlice []string + var nilSortSlice []model.Sort + defaultSort := model.Sort{Field: "name", Direction: "asc"} + + assert.Equal(t, nil, err) + assert.Equal(t, 0, p.Offset) + assert.Equal(t, 10, p.Limit) + assert.Equal(t, nilStringSlice, p.Expand) + assert.Equal(t, nilSortSlice, p.Sort) + assert.Equal(t, []model.Sort{defaultSort}, p.GetSort(defaultSort)) + }) +} + +func TestGetQueryVarInt(t *testing.T) { + type want struct { + val int + err string + } + + tests := []struct { + name string + url string + queryVar string + required bool + defaultValue int + want want + }{ + { + name: "simple default", + url: "/hosts", + queryVar: "something", + required: false, + defaultValue: 100, + want: want{ + val: 100, + err: "", + }, + }, + { + name: "required flag", + url: "/hosts", + queryVar: "something", + required: true, + want: want{ + val: 0, + err: "something was not supplied in the request", + }, + }, + { + name: "simple get", + url: "/hosts?something=50", + queryVar: "something", + required: true, + want: want{ + val: 50, + err: "", + }, + }, + { + name: "invalid number", + url: "/hosts?something=aaa", + queryVar: "something", + required: true, + want: want{ + val: 0, + err: "", + }, + }, + { + name: "preceding zeros", + url: "/hosts?something=0000050", + queryVar: "something", + required: true, + want: want{ + val: 50, + err: "", + }, + }, + { + name: "decimals", + url: "/hosts?something=50.50", + queryVar: "something", + required: true, + want: want{ + val: 0, + err: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, tt.url, nil) + val, err := getQueryVarInt(r, tt.queryVar, tt.required, tt.defaultValue) + assert.Equal(t, tt.want.val, val) + if tt.want.err != "" { + assert.NotEqual(t, nil, err) + assert.Equal(t, tt.want.err, err.Error()) + } + }) + } +} diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go new file mode 100644 index 000000000..c902b57db --- /dev/null +++ b/backend/internal/api/handler/hosts.go @@ -0,0 +1,215 @@ +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/jobqueue" + "npm/internal/logger" + "npm/internal/nginx" + "npm/internal/validator" + + "gorm.io/gorm" +) + +// 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), middleware.GetExpandFromContext(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 uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := host.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // nolint: errcheck,gosec + item.Expand(middleware.GetExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// 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).(uint) + newHost.UserID = userID + + if err = validator.ValidateHost(newHost); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newHost.Save(false); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil) + return + } + + if newHost.UpstreamID.Uint > 0 { + // nolint: errcheck, gosec + newHost.Expand([]string{"upstream"}) + } + + configureHost(newHost) + + h.ResultResponseJSON(w, r, http.StatusOK, newHost) + } +} + +// UpdateHost updates a host +// Route: PUT /hosts/{hostID} +func UpdateHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + hostObject, err := host.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &hostObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = validator.ValidateHost(hostObject); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = hostObject.Save(false); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + // nolint: errcheck,gosec + hostObject.Expand(middleware.GetExpandFromContext(r)) + + configureHost(hostObject) + + h.ResultResponseJSON(w, r, http.StatusOK, hostObject) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteHost removes a host +// Route: DELETE /hosts/{hostID} +func DeleteHost() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := host.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + configureHost(item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// GetHostNginxConfig will return a Host's nginx config from disk +// Route: GET /hosts/{hostID}/nginx-config +// Route: GET /hosts/{hostID}/nginx-config.txt +func GetHostNginxConfig(format string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var hostID uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := host.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // Get the config from disk + content, nErr := nginx.GetHostConfigContent(item) + if nErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, nErr.Error(), nil) + return + } + if format == "text" { + h.ResultResponseText(w, http.StatusOK, content) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, content) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +func configureHost(hst host.Model) { + err := jobqueue.AddJob(jobqueue.Job{ + Name: "NginxConfigureHost", + Action: func() error { + return nginx.ConfigureHost(hst) + }, + }) + if err != nil { + logger.Error("ConfigureHostError", err) + } +} diff --git a/backend/internal/api/handler/nginx_templates.go b/backend/internal/api/handler/nginx_templates.go new file mode 100644 index 000000000..f5df7e06c --- /dev/null +++ b/backend/internal/api/handler/nginx_templates.go @@ -0,0 +1,142 @@ +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/nginxtemplate" + + "gorm.io/gorm" +) + +// GetNginxTemplates will return a list of Nginx Templates +// Route: GET /nginx-templates +func GetNginxTemplates() 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 := nginxtemplate.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) + } + } +} + +// GetNginxTemplate will return a single Nginx Template +// Route: GET /nginx-templates/{templateID} +func GetNginxTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID uint + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := nginxtemplate.GetByID(templateID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// CreateNginxTemplate will create a Nginx Template +// Route: POST /nginx-templates +func CreateNginxTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newNginxTemplate nginxtemplate.Model + err := json.Unmarshal(bodyBytes, &newNginxTemplate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) + newNginxTemplate.UserID = userID + + if err = newNginxTemplate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Nginx Template: %s", err.Error()), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, newNginxTemplate) + } +} + +// UpdateNginxTemplate updates a nginx template +// Route: PUT /nginx-templates/{templateID} +func UpdateNginxTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID uint + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + // reconfigure, _ := getQueryVarBool(r, "reconfigure", false, false) + + nginxTemplate, err := nginxtemplate.GetByID(templateID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &nginxTemplate) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if err = nginxTemplate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, nginxTemplate) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteNginxTemplate removes a nginx template +// Route: DELETE /nginx-templates/{templateID} +func DeleteNginxTemplate() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var templateID uint + if templateID, err = getURLParamInt(r, "templateID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := nginxtemplate.GetByID(templateID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} diff --git a/backend/internal/api/handler/not_allowed.go b/backend/internal/api/handler/not_allowed.go new file mode 100644 index 000000000..966debab3 --- /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 000000000..761260423 --- /dev/null +++ b/backend/internal/api/handler/not_found.go @@ -0,0 +1,82 @@ +package handler + +import ( + "io" + "io/fs" + "mime" + "net/http" + "path/filepath" + "strings" + + "npm/embed" + h "npm/internal/api/http" + + "github.com/rotisserie/eris" +) + +var ( + assetsSub fs.FS + errIsDir = eris.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(embed.Assets, "assets") + + return func(w http.ResponseWriter, r *http.Request) { + defaultFile := "index.html" + path := strings.TrimLeft(r.URL.Path, "/") + + isAPI := false + if len(path) >= 3 && path[0:3] == "api" { + isAPI = true + } + + if path == "" { + path = defaultFile + } + + err := tryRead(assetsSub, path, w) + if err == errIsDir { + err = tryRead(assetsSub, defaultFile, w) + if err != nil { + h.NotFound(w, r) + } + } else if err == nil { + return + } + + // Check if the path has an extension and not in the "/api" path + ext := filepath.Ext(path) + if !isAPI && ext == "" { + // Not an api endpoint and Not a specific file, return the default index file + err := tryRead(assetsSub, defaultFile, w) + if err == nil { + return + } + } + + h.NotFound(w, r) + } +} + +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/oauth.go b/backend/internal/api/handler/oauth.go new file mode 100644 index 000000000..f88c5b846 --- /dev/null +++ b/backend/internal/api/handler/oauth.go @@ -0,0 +1,156 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + h "npm/internal/api/http" + "npm/internal/entity/auth" + "npm/internal/entity/setting" + "npm/internal/entity/user" + "npm/internal/errors" + njwt "npm/internal/jwt" + "npm/internal/logger" + + "gorm.io/gorm" +) + +// getRequestIPAddress will use X-FORWARDED-FOR header if it exists +// otherwise it will use RemoteAddr +func getRequestIPAddress(r *http.Request) string { + // this Get is case insensitive + xff := r.Header.Get("X-FORWARDED-FOR") + if xff != "" { + ip, _, _ := strings.Cut(xff, ",") + return strings.TrimSpace(ip) + } + return r.RemoteAddr +} + +// OAuthLogin ... +// Route: GET /oauth/login +func OAuthLogin() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if !setting.AuthMethodEnabled(auth.TypeOAuth) { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + return + } + + redirectBase, _ := getQueryVarString(r, "redirect_base", false, "") + url, err := auth.OAuthLogin(redirectBase, getRequestIPAddress(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + h.ResultResponseJSON(w, r, http.StatusOK, url) + } +} + +// OAuthRedirect ... +// Route: GET /oauth/redirect +func OAuthRedirect() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if !setting.AuthMethodEnabled(auth.TypeOAuth) { + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + return + } + + code, err := getQueryVarString(r, "code", true, "") + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + ou, err := auth.OAuthReturn(r.Context(), code, getRequestIPAddress(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if ou.Identifier == "" { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "User found, but OAuth identifier seems misconfigured", nil) + return + } + + jwt, err := newTokenOAuth(ou) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) + return + } + + // encode jwt to json + j, _ := json.Marshal(jwt) + + // Redirect to frontend with success + http.Redirect(w, r, fmt.Sprintf("/?token_response=%s", url.QueryEscape(string(j))), http.StatusSeeOther) + } +} + +// newTokenOAuth takes a OAuthUser and creates a new token, +// optionally creating a new user if one does not exist +func newTokenOAuth(ou *auth.OAuthUser) (*njwt.GeneratedResponse, error) { + // Get OAuth settings + oAuthSettings, err := setting.GetOAuthSettings() + if err != nil { + logger.Error("OAuth settings not found", err) + return nil, err + } + + // Get Auth by identity + authObj, authErr := auth.GetByIdenityType(ou.GetID(), auth.TypeOAuth) + if authErr == gorm.ErrRecordNotFound { + // Auth is not found for this identity. We can create it + if !oAuthSettings.AutoCreateUser { + // user does not have an auth record + // and auto create is disabled. Showing account disabled error + // for the time being + return nil, errors.ErrUserDisabled + } + + // Attempt to find user by email + foundUser, err := user.GetByEmail(ou.GetEmail()) + if err == gorm.ErrRecordNotFound { + // User not found, create user + foundUser, err = user.CreateFromOAuthUser(ou) + if err != nil { + logger.Error("user.CreateFromOAuthUser", err) + return nil, err + } + logger.Info("Created user from OAuth: %s, %s", ou.GetID(), foundUser.Email) + } else if err != nil { + logger.Error("user.GetByEmail", err) + return nil, err + } + + // Create auth record and attach to this user + authObj = auth.Model{ + UserID: foundUser.ID, + Type: auth.TypeOAuth, + Identity: ou.GetID(), + } + if err := authObj.Save(); err != nil { + logger.Error("auth.Save", err) + return nil, err + } + logger.Info("Created OAuth auth for user: %s, %s", ou.GetID(), foundUser.Email) + } else if authErr != nil { + logger.Error("auth.GetByIdenityType", err) + return nil, authErr + } + + userObj, userErr := user.GetByID(authObj.UserID) + if userErr != nil { + return nil, userErr + } + + if userObj.IsDisabled { + return nil, errors.ErrUserDisabled + } + + jwt, err := njwt.Generate(&userObj, false) + return &jwt, err +} diff --git a/backend/internal/api/handler/schema.go b/backend/internal/api/handler/schema.go new file mode 100644 index 000000000..d7a3f0cf1 --- /dev/null +++ b/backend/internal/api/handler/schema.go @@ -0,0 +1,111 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io/fs" + "net/http" + "strings" + + "npm/embed" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/logger" + + "github.com/jc21/jsref" + "github.com/jc21/jsref/provider" +) + +var ( + swaggerSchema []byte + apiDocsSub fs.FS +) + +// 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, _ *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 { + apiDocsSub, _ = fs.Sub(embed.APIDocFiles, "api_docs") + + // nolint:gosec + swaggerSchema, _ = fs.ReadFile(apiDocsSub, "api.swagger.json") + + // Replace {{VERSION}} with Config Version + swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version)) + + // Dereference the JSON Schema: + var sch any + if err := json.Unmarshal(swaggerSchema, &sch); err != nil { + logger.Error("SwaggerUnmarshalError", err) + return nil + } + + prov := provider.NewIoFS(apiDocsSub, "") + resolver := jsref.New() + err := resolver.AddProvider(prov) + if err != nil { + logger.Error("SchemaProviderError", err) + } + + result, err := resolver.Resolve(sch, "", []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.CreateNginxTemplate}}"`, schema.CreateNginxTemplate()) + str = strings.ReplaceAll(str, `"{{schema.UpdateNginxTemplate}}"`, schema.UpdateNginxTemplate()) + + 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()) + + str = strings.ReplaceAll(str, `"{{schema.CreateUpstream}}"`, schema.CreateUpstream()) + str = strings.ReplaceAll(str, `"{{schema.UpdateUpstream}}"`, schema.UpdateUpstream()) + + return []byte(str) +} diff --git a/backend/internal/api/handler/settings.go b/backend/internal/api/handler/settings.go new file mode 100644 index 000000000..b6c908909 --- /dev/null +++ b/backend/internal/api/handler/settings.go @@ -0,0 +1,110 @@ +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/v5" + "gorm.io/gorm" +) + +// 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") + + item, err := setting.GetByName(name) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// 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 + } + + // Check if the setting already exists + if _, err := setting.GetByName(newSetting.Name); err == nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Setting with name '%s' already exists", newSetting.Name), 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 updates a setting +// Route: PUT /settings/{name} +// TODO: Add validation for the setting value, for system settings they should be validated against the setting name and type +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) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + if err := json.Unmarshal(bodyBytes, &setting); 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) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} diff --git a/backend/internal/api/handler/sse_notification.go b/backend/internal/api/handler/sse_notification.go new file mode 100644 index 000000000..998af5f35 --- /dev/null +++ b/backend/internal/api/handler/sse_notification.go @@ -0,0 +1,28 @@ +package handler + +import ( + "encoding/json" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/serverevents" +) + +// TestSSENotification specifically fires of a SSE message for testing purposes +// Route: POST /sse-notification +func TestSSENotification() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var msg serverevents.Message + err := json.Unmarshal(bodyBytes, &msg) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + serverevents.Send(msg, "") + h.ResultResponseJSON(w, r, http.StatusOK, true) + } +} diff --git a/backend/internal/api/handler/streams.go b/backend/internal/api/handler/streams.go new file mode 100644 index 000000000..2b097705f --- /dev/null +++ b/backend/internal/api/handler/streams.go @@ -0,0 +1,140 @@ +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" + + "gorm.io/gorm" +) + +// 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 uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := stream.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// 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).(uint) + 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 updates a stream +// 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 uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + host, err := stream.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + 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) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteStream removes a stream +// 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 uint + if hostID, err = getURLParamInt(r, "hostID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := stream.GetByID(hostID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} diff --git a/backend/internal/api/handler/upstreams.go b/backend/internal/api/handler/upstreams.go new file mode 100644 index 000000000..4b1a4e785 --- /dev/null +++ b/backend/internal/api/handler/upstreams.go @@ -0,0 +1,214 @@ +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/entity/upstream" + "npm/internal/jobqueue" + "npm/internal/logger" + "npm/internal/nginx" + "npm/internal/validator" + + "gorm.io/gorm" +) + +// GetUpstreams will return a list of Upstreams +// Route: GET /upstreams +func GetUpstreams() 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 := upstream.List(pageInfo, middleware.GetFiltersFromContext(r), middleware.GetExpandFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, items) + } + } +} + +// GetUpstream will return a single Upstream +// Route: GET /upstreams/{upstreamID} +func GetUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID uint + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // nolint: errcheck,gosec + item.Expand(middleware.GetExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// CreateUpstream will create a Upstream +// Route: POST /upstreams +func CreateUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newUpstream upstream.Model + err := json.Unmarshal(bodyBytes, &newUpstream) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(uint) + newUpstream.UserID = userID + + if err = validator.ValidateUpstream(newUpstream); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newUpstream.Save(false); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Upstream: %s", err.Error()), nil) + return + } + + configureUpstream(newUpstream) + + h.ResultResponseJSON(w, r, http.StatusOK, newUpstream) + } +} + +// UpdateUpstream updates a stream +// Route: PUT /upstreams/{upstreamID} +func UpdateUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID uint + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + 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 = validator.ValidateUpstream(item); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = item.Save(false); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + // nolint: errcheck,gosec + // item.Expand(middleware.GetExpandFromContext(r)) + + configureUpstream(item) + + h.ResultResponseJSON(w, r, http.StatusOK, item) + } + } +} + +// DeleteUpstream removes a upstream +// Route: DELETE /upstreams/{upstreamID} +func DeleteUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID uint + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // Ensure that this upstream isn't in use by a host + cnt := host.GetUpstreamUseCount(upstreamID) + if cnt > 0 { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot delete upstream that is in use by at least 1 host", nil) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + configureUpstream(item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// GetUpstreamNginxConfig will return a Host's nginx config from disk +// Route: GET /upstreams/{upstreamID}/nginx-config +// Route: GET /upstreams/{upstreamID}/nginx-config.txt +func GetUpstreamNginxConfig(format string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID uint + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // Get the config from disk + content, nErr := nginx.GetUpstreamConfigContent(item) + if nErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, nErr.Error(), nil) + return + } + if format == "text" { + h.ResultResponseText(w, http.StatusOK, content) + return + } + h.ResultResponseJSON(w, r, http.StatusOK, content) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +func configureUpstream(u upstream.Model) { + err := jobqueue.AddJob(jobqueue.Job{ + Name: "NginxConfigureUpstream", + Action: func() error { + return nginx.ConfigureUpstream(u) + }, + }) + if err != nil { + logger.Error("ConfigureUpstreamError", err) + } +} diff --git a/backend/internal/api/handler/users.go b/backend/internal/api/handler/users.go new file mode 100644 index 000000000..a15848f57 --- /dev/null +++ b/backend/internal/api/handler/users.go @@ -0,0 +1,333 @@ +package handler + +import ( + "encoding/json" + "net/http" + "time" + + 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/v5" + "gorm.io/gorm" +) + +type setAuthModel struct { + // The json tags are required, as the change password form decodes into this object + Type string `json:"type"` + Secret string `json:"secret"` + CurrentSecret string `json:"current_secret"` +} + +// GetUsers returns all users +// 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), middleware.GetExpandFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, users) + } + } +} + +// GetUser returns a specific user +// 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 + } + + item, err := user.GetByID(userID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // nolint: errcheck,gosec + item.Expand(middleware.GetExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// UpdateUser updates a user +// 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 + } + + userObject, err := user.GetByID(userID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + // nolint: errcheck,gosec + userObject.Expand([]string{"capabilities"}) + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + err := json.Unmarshal(bodyBytes, &userObject) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + if userObject.IsDisabled && self { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil) + return + } + + if err = userObject.Save(); err != nil { + if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + logger.Error("UpdateUserError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + if !self { + err = userObject.SaveCapabilities() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + } + + // nolint: errcheck,gosec + userObject.Expand(middleware.GetExpandFromContext(r)) + + h.ResultResponseJSON(w, r, http.StatusOK, userObject) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// DeleteUser removes a user +// Route: DELETE /users/{userID} +func DeleteUser() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var userID uint + 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).(uint) + if myUserID == userID { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil) + return + } + + item, err := user.GetByID(userID) + switch err { + case gorm.ErrRecordNotFound: + h.NotFound(w, r) + case nil: + if err := item.Delete(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, true) + } + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// CreateUser creates a user +// 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 || err == errors.ErrSystemUserReadonly { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + logger.Error("UpdateUserError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil) + } + return + } + + // Set the permissions to full-admin for this user + if !config.IsSetup { + newUser.Capabilities = []string{user.CapabilityFullAdmin} + } + + // nolint: errcheck,gosec + err = newUser.SaveCapabilities() + if err != nil { + h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), 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.TypeLocal { + 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) (uint, bool, error) { + userIDstr := chi.URLParam(r, "userID") + selfUserID, _ := r.Context().Value(c.UserIDCtxKey).(uint) + + var userID uint + self := false + if userIDstr == "me" { + // Get user id from Token + userID = selfUserID + self = true + } else { + var userIDerr error + if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil { + return 0, false, userIDerr + } + self = selfUserID == userID + } + return userID, self, nil +} + +// SetAuth sets a auth method. This can be used for "me" and `2` for example +// 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) + + var newAuth setAuthModel + err := json.Unmarshal(bodyBytes, &newAuth) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + userID, isSelf, userIDErr := getUserIDFromRequest(r) + if userIDErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) + return + } + + // Load user + thisUser, thisUserErr := user.GetByID(userID) + if thisUserErr == gorm.ErrRecordNotFound { + h.NotFound(w, r) + return + } else if thisUserErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil) + return + } + + if thisUser.IsSystem { + h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil) + return + } + + // Load existing auth for user + userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type) + if userAuthErr != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil) + return + } + + if isSelf { + // confirm that the current_secret given is valid for the one stored in the database + validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret) + if validateErr != nil { + logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error()) + // Sleep for 1 second to prevent brute force password guessing + time.Sleep(time.Second) + + h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil) + return + } + } + + if newAuth.Type == auth.TypeLocal { + err := userAuth.SetPassword(newAuth.Secret) + if err != nil { + logger.Error("SetPasswordError", err) + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } + + if err = userAuth.Save(); err != nil { + logger.Error("AuthSaveError", err) + h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) + return + } + + userAuth.Secret = "" + + // todo: add to audit-log + + h.ResultResponseJSON(w, r, http.StatusOK, userAuth) + } +} diff --git a/backend/internal/api/http/responses.go b/backend/internal/api/http/responses.go new file mode 100644 index 000000000..f0aa25c3b --- /dev/null +++ b/backend/internal/api/http/responses.go @@ -0,0 +1,114 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + c "npm/internal/api/context" + "npm/internal/errors" + "npm/internal/logger" + + "github.com/qri-io/jsonschema" + "github.com/rotisserie/eris" +) + +var ( + // ErrInvalidPayload is an error for invalid incoming data + ErrInvalidPayload = eris.New("Payload is invalid") +) + +// Response interface for standard API results +type Response struct { + Result any `json:"result"` + Error any `json:"error,omitempty"` +} + +// ErrorResponse interface for errors returned via the API +type ErrorResponse struct { + Code any `json:"code"` + Message any `json:"message"` + Invalid any `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 any) { + 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, errs []jsonschema.KeyError) { + errorResponse := ErrorResponse{ + Code: http.StatusBadRequest, + Message: errors.ErrValidationFailed, + Invalid: errs, + } + + 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 any) { + errorResponse := ErrorResponse{ + Code: status, + Message: message, + Invalid: extended, + } + + ResultResponseJSON(w, r, status, errorResponse) +} + +// NotFound will return a 404 response +func NotFound(w http.ResponseWriter, r *http.Request) { + errorResponse := ErrorResponse{ + Code: http.StatusNotFound, + Message: "Not found", + } + + ResultResponseJSON(w, r, http.StatusNotFound, errorResponse) +} + +// ResultResponseText will write the result as text to the http output +func ResultResponseText(w http.ResponseWriter, status int, content string) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(status) + fmt.Fprint(w, content) +} + +// 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/http/responses_test.go b/backend/internal/api/http/responses_test.go new file mode 100644 index 000000000..afcaa4ffd --- /dev/null +++ b/backend/internal/api/http/responses_test.go @@ -0,0 +1,195 @@ +package http + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "npm/internal/entity/user" + "npm/internal/model" + + "github.com/qri-io/jsonschema" + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestResultResponseJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + status int + given any + want string + }{ + { + name: "simple response", + status: http.StatusOK, + given: true, + want: "{\"result\":true}", + }, + { + name: "detailed response", + status: http.StatusBadRequest, + given: user.Model{ + Base: model.Base{ID: 10}, + Email: "me@example.com", + Name: "John Doe", + }, + want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}", + }, + { + name: "error response", + status: http.StatusNotFound, + given: ErrorResponse{ + Code: 404, + Message: "Not found", + Invalid: []string{"your", "page", "was", "not", "found"}, + }, + want: "{\"result\":null,\"error\":{\"code\":404,\"message\":\"Not found\",\"invalid\":[\"your\",\"page\",\"was\",\"not\",\"found\"]}}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/anything", nil) + w := httptest.NewRecorder() + ResultResponseJSON(w, r, tt.status, tt.given) + res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + assert.Equal(t, tt.want, string(body)) + assert.Equal(t, tt.status, res.StatusCode) + assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type")) + }) + } +} + +func TestResultSchemaErrorJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + given []jsonschema.KeyError + want string + }{ + { + name: "case a", + given: []jsonschema.KeyError{ + { + PropertyPath: "/something", + InvalidValue: "name", + Message: "Name cannot be empty", + }, + }, + want: "{\"result\":null,\"error\":{\"code\":400,\"message\":{},\"invalid\":[{\"propertyPath\":\"/something\",\"invalidValue\":\"name\",\"message\":\"Name cannot be empty\"}]}}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/anything", nil) + w := httptest.NewRecorder() + ResultSchemaErrorJSON(w, r, tt.given) + res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + assert.Equal(t, tt.want, string(body)) + assert.Equal(t, 400, res.StatusCode) + assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type")) + }) + } +} + +func TestResultErrorJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + status int + message string + extended any + want string + }{ + { + name: "case a", + status: http.StatusBadGateway, + message: "Oh not something is not acceptable", + extended: nil, + want: "{\"result\":null,\"error\":{\"code\":502,\"message\":\"Oh not something is not acceptable\"}}", + }, + { + name: "case b", + status: http.StatusNotAcceptable, + message: "Oh not something is not acceptable again", + extended: []string{"name is not allowed", "dob is wrong or something"}, + want: "{\"result\":null,\"error\":{\"code\":406,\"message\":\"Oh not something is not acceptable again\",\"invalid\":[\"name is not allowed\",\"dob is wrong or something\"]}}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/anything", nil) + w := httptest.NewRecorder() + ResultErrorJSON(w, r, tt.status, tt.message, tt.extended) + res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + assert.Equal(t, tt.want, string(body)) + assert.Equal(t, tt.status, res.StatusCode) + assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type")) + }) + } +} + +func TestNotFound(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/anything", nil) + w := httptest.NewRecorder() + NotFound(w, r) + res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + assert.Equal(t, "{\"result\":null,\"error\":{\"code\":404,\"message\":\"Not found\"}}", string(body)) + assert.Equal(t, http.StatusNotFound, res.StatusCode) + assert.Equal(t, "application/json; charset=utf-8", res.Header.Get("Content-Type")) + }) +} + +func TestResultResponseText(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("basic test", func(t *testing.T) { + w := httptest.NewRecorder() + ResultResponseText(w, http.StatusOK, "omg this works") + res := w.Result() + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + if err != nil { + t.Errorf("expected error to be nil got %v", err) + } + assert.Equal(t, "omg this works", string(body)) + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "text/plain; charset=utf-8", res.Header.Get("Content-Type")) + }) +} diff --git a/backend/internal/api/middleware/access_control.go b/backend/internal/api/middleware/access_control.go new file mode 100644 index 000000000..18bca31b1 --- /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/access_control_test.go b/backend/internal/api/middleware/access_control_test.go new file mode 100644 index 000000000..6f9b09932 --- /dev/null +++ b/backend/internal/api/middleware/access_control_test.go @@ -0,0 +1,29 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "npm/internal/api/middleware" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestAccessControl(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + rr := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + assert.Nil(t, err) + accessControl := middleware.AccessControl(handler) + accessControl.ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin")) +} diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go new file mode 100644 index 000000000..5d71f5052 --- /dev/null +++ b/backend/internal/api/middleware/auth.go @@ -0,0 +1,101 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + "slices" + + 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/v5" +) + +// DecodeAuth decodes an auth header +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.Verify(tokenAuth, jwtauth.TokenFromHeader, jwtauth.TokenFromQuery) +} + +// 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(permissions ...string) 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 := uint(claims["uid"].(float64)) + _, enabled, _ := user.IsEnabled(userID) + if token == nil || !enabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + // Check if permissions exist for this user + if len(permissions) > 0 { + // Since the permission that we require is not on the token, we have to get it from the DB + // So we don't go crazy with hits, we will use a memory cache + cacheKey := fmt.Sprintf("userCapabilties.%v", userID) + cacheItem, found := AuthCache.Get(cacheKey) + + var userCapabilities []string + if found { + userCapabilities = cacheItem.([]string) + } else { + // Get from db and store it + userCapabilities, err = user.GetCapabilities(userID) + if err != nil { + AuthCacheSet(cacheKey, userCapabilities) + } + } + + // Now check that they have the permission in their admin capabilities + // full-admin can do anything + hasOnePermission := false + for _, permission := range permissions { + if slices.Contains(userCapabilities, user.CapabilityFullAdmin) || slices.Contains(userCapabilities, permission) { + hasOnePermission = true + } + } + + if !hasOnePermission { + // Access denied + logger.Debug("Enforce Failed: User has %v but needs %v", userCapabilities, permissions) + h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", 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/auth_cache.go b/backend/internal/api/middleware/auth_cache.go new file mode 100644 index 000000000..e0ba5a1ee --- /dev/null +++ b/backend/internal/api/middleware/auth_cache.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "time" + + "npm/internal/logger" + + cache "github.com/patrickmn/go-cache" +) + +// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints +var AuthCache *cache.Cache + +// AuthCacheInit will create a new Memory Cache +func AuthCacheInit() { + logger.Debug("Creating a new AuthCache") + AuthCache = cache.New(1*time.Minute, 5*time.Minute) +} + +// AuthCacheSet will store the item in memory for the expiration time +func AuthCacheSet(k string, x any) { + AuthCache.Set(k, x, cache.DefaultExpiration) +} diff --git a/backend/internal/api/middleware/body_context.go b/backend/internal/api/middleware/body_context.go new file mode 100644 index 000000000..53535b285 --- /dev/null +++ b/backend/internal/api/middleware/body_context.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "context" + "io" + "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, _ = io.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/body_context_test.go b/backend/internal/api/middleware/body_context_test.go new file mode 100644 index 000000000..603b0a56f --- /dev/null +++ b/backend/internal/api/middleware/body_context_test.go @@ -0,0 +1,43 @@ +package middleware_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + c "npm/internal/api/context" + "npm/internal/api/middleware" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestBodyContext(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Create a test request with a body + body := []byte(`{"name": "John", "age": 30}`) + req, err := http.NewRequest("POST", "/test", bytes.NewBuffer(body)) + assert.Nil(t, err) + + // Create a test response recorder + rr := httptest.NewRecorder() + + // Create a test handler that checks the context for the body data + handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + bodyData := r.Context().Value(c.BodyCtxKey).([]byte) + assert.Equal(t, body, bodyData) + }) + + // Wrap the handler with the BodyContext middleware + mw := middleware.BodyContext()(handler) + + // Call the middleware with the test request and response recorder + mw.ServeHTTP(rr, req) + + // Check that the response status code is 200 + status := rr.Code + assert.Equal(t, http.StatusOK, status) +} diff --git a/backend/internal/api/middleware/cors.go b/backend/internal/api/middleware/cors.go new file mode 100644 index 000000000..15489b93f --- /dev/null +++ b/backend/internal/api/middleware/cors.go @@ -0,0 +1,82 @@ +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" +) + +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", +} + +// Cors handles cors headers +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 handles options requests +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/cors_test.go b/backend/internal/api/middleware/cors_test.go new file mode 100644 index 000000000..da51638c1 --- /dev/null +++ b/backend/internal/api/middleware/cors_test.go @@ -0,0 +1,79 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "npm/internal/api/middleware" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" +) + +func TestCors(t *testing.T) { + r := chi.NewRouter() + r.Use(middleware.Cors(r)) + + r.Get("/test", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("test")) + }) + + req, err := http.NewRequest("GET", "/test", nil) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, "GET,OPTIONS", rr.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "Authorization,Host,Content-Type,Connection,User-Agent,Cache-Control,Accept-Encoding", rr.Header().Get("Access-Control-Allow-Headers")) + assert.Equal(t, "test", rr.Body.String()) +} + +func TestCorsNoRoute(t *testing.T) { + r := chi.NewRouter() + r.Use(middleware.Cors(r)) + + req, err := http.NewRequest("GET", "/test", nil) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Headers")) +} + +func TestOptions(t *testing.T) { + r := chi.NewRouter() + r.Use(middleware.Options(r)) + + r.Get("/test", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("test")) + }) + + req, err := http.NewRequest("OPTIONS", "/test", nil) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + assert.Equal(t, "{}", rr.Body.String()) +} + +func TestOptionsNoRoute(t *testing.T) { + r := chi.NewRouter() + r.Use(middleware.Options(r)) + + req, err := http.NewRequest("OPTIONS", "/test", nil) + assert.Nil(t, err) + + rr := httptest.NewRecorder() + r.ServeHTTP(rr, req) + + assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Headers")) +} diff --git a/backend/internal/api/middleware/enforce_setup.go b/backend/internal/api/middleware/enforce_setup.go new file mode 100644 index 000000000..49f267c47 --- /dev/null +++ b/backend/internal/api/middleware/enforce_setup.go @@ -0,0 +1,23 @@ +package middleware + +import ( + "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() 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 { + h.ResultErrorJSON(w, r, http.StatusForbidden, "Not available during setup phase", nil) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/enforce_setup_test.go b/backend/internal/api/middleware/enforce_setup_test.go new file mode 100644 index 000000000..dba7cf1fb --- /dev/null +++ b/backend/internal/api/middleware/enforce_setup_test.go @@ -0,0 +1,50 @@ +package middleware_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "npm/internal/api/middleware" + "npm/internal/config" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestEnforceSetup(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + isSetup bool + expectedCode int + }{ + { + name: "should allow request when setup is expected and is setup", + isSetup: true, + expectedCode: http.StatusOK, + }, + { + name: "should error when setup is expected but not setup", + isSetup: false, + expectedCode: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.IsSetup = tt.isSetup + + handler := middleware.EnforceSetup()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + assert.Equal(t, tt.expectedCode, w.Code) + }) + } +} diff --git a/backend/internal/api/middleware/expansion.go b/backend/internal/api/middleware/expansion.go new file mode 100644 index 000000000..266bfe42f --- /dev/null +++ b/backend/internal/api/middleware/expansion.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + c "npm/internal/api/context" +) + +// Expansion will determine whether the request should have objects expanded +// with ?expand=item,item +func Expansion(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expandStr := r.URL.Query().Get("expand") + if expandStr != "" { + ctx := r.Context() + ctx = context.WithValue(ctx, c.ExpansionCtxKey, strings.Split(expandStr, ",")) + next.ServeHTTP(w, r.WithContext(ctx)) + } else { + next.ServeHTTP(w, r) + } + }) +} + +// GetExpandFromContext returns the Expansion setting +func GetExpandFromContext(r *http.Request) []string { + expand, ok := r.Context().Value(c.ExpansionCtxKey).([]string) + if !ok { + return nil + } + return expand +} diff --git a/backend/internal/api/middleware/expansion_test.go b/backend/internal/api/middleware/expansion_test.go new file mode 100644 index 000000000..47904d5c0 --- /dev/null +++ b/backend/internal/api/middleware/expansion_test.go @@ -0,0 +1,76 @@ +package middleware_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + c "npm/internal/api/context" + "npm/internal/api/middleware" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestExpansion(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("with expand query param", func(t *testing.T) { + req, err := http.NewRequest("GET", "/path?expand=item1,item2", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + + handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + expand := middleware.GetExpandFromContext(r) + assert.Equal(t, []string{"item1", "item2"}, expand) + }) + + middleware.Expansion(handler).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("without expand query param", func(t *testing.T) { + req, err := http.NewRequest("GET", "/path", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + + handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + expand := middleware.GetExpandFromContext(r) + assert.Nil(t, expand) + }) + + middleware.Expansion(handler).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + }) +} + +func TestGetExpandFromContext(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Run("with context value", func(t *testing.T) { + req, err := http.NewRequest("GET", "/path", nil) + assert.NoError(t, err) + + ctx := req.Context() + ctx = context.WithValue(ctx, c.ExpansionCtxKey, []string{"item1", "item2"}) + req = req.WithContext(ctx) + + expand := middleware.GetExpandFromContext(req) + assert.Equal(t, []string{"item1", "item2"}, expand) + }) + + t.Run("without context value", func(t *testing.T) { + req, err := http.NewRequest("GET", "/path", nil) + assert.NoError(t, err) + + expand := middleware.GetExpandFromContext(req) + assert.Nil(t, expand) + }) +} diff --git a/backend/internal/api/middleware/list_query.go b/backend/internal/api/middleware/list_query.go new file mode 100644 index 000000000..9660f70b8 --- /dev/null +++ b/backend/internal/api/middleware/list_query.go @@ -0,0 +1,195 @@ +package middleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/model" + "npm/internal/tags" + "npm/internal/util" + + "github.com/qri-io/jsonschema" +) + +// ListQuery 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 +// and the sort parameter is valid as well. +// After we have determined what the Filters are to be, they are saved on the Context +// to be used later in other endpoints. +func ListQuery(obj any) func(http.Handler) http.Handler { + schemaData := tags.GetFilterSchema(obj) + filterMap := tags.GetFilterMap(obj, "") + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx, statusCode, errMsg, errors := listQueryFilters(ctx, r, schemaData) + if statusCode > 0 { + h.ResultErrorJSON(w, r, statusCode, errMsg, errors) + return + } + + ctx, statusCode, errMsg = listQuerySort(ctx, r, filterMap) + if statusCode > 0 { + h.ResultErrorJSON(w, r, statusCode, errMsg, nil) + return + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func listQuerySort( + ctx context.Context, + r *http.Request, + filterMap map[string]model.FilterMapValue, +) (context.Context, int, string) { + var sortFields []model.Sort + + sortString := r.URL.Query().Get("sort") + if sortString == "" { + return ctx, 0, "" + } + + // 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", + }) + } + } + + // check against filter schema + for _, f := range sortFields { + if _, exists := filterMap[f.Field]; !exists { + return ctx, http.StatusBadRequest, "Invalid sort field" + } + } + + ctx = context.WithValue(ctx, c.SortCtxKey, sortFields) + + // No problems! + return ctx, 0, "" +} + +func listQueryFilters( + ctx context.Context, + r *http.Request, + schemaData string, +) (context.Context, int, string, any) { + reservedFilterKeys := []string{ + "limit", + "offset", + "sort", + "expand", + "t", // This is used as a timestamp paramater in some clients and can be ignored + } + + 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 { + // Marshal the Filters in to a JSON string so that the Schema Validation works against it + filterData, marshalErr := json.MarshalIndent(filters, "", " ") + if marshalErr != nil { + return ctx, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil + } + + // Create root schema + rs := &jsonschema.Schema{} + if err := json.Unmarshal([]byte(schemaData), rs); err != nil { + return ctx, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil + } + + // Validate it + errors, jsonError := rs.ValidateBytes(ctx, filterData) + if jsonError != nil { + return ctx, http.StatusBadRequest, jsonError.Error(), nil + } + + if len(errors) > 0 { + return ctx, http.StatusBadRequest, "Invalid Filters", errors + } + + ctx = context.WithValue(ctx, c.FiltersCtxKey, filters) + } + + // No problems! + return ctx, 0, "", nil +} + +// 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 + return nil + } + return filters +} + +// GetSortFromContext returns the Sort +func GetSortFromContext(r *http.Request) []model.Sort { + sorts, ok := r.Context().Value(c.SortCtxKey).([]model.Sort) + if !ok { + // the assertion failed + return nil + } + return sorts +} diff --git a/backend/internal/api/middleware/list_query_test.go b/backend/internal/api/middleware/list_query_test.go new file mode 100644 index 000000000..3563c1fe1 --- /dev/null +++ b/backend/internal/api/middleware/list_query_test.go @@ -0,0 +1,99 @@ +package middleware_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + c "npm/internal/api/context" + "npm/internal/api/middleware" + "npm/internal/entity/user" + "npm/internal/model" + "npm/internal/tags" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestListQuery(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + queryParams string + expectedStatus int + }{ + { + name: "valid query params", + queryParams: "?name:contains=John&sort=name.desc", + expectedStatus: http.StatusOK, + }, + { + name: "invalid sort field", + queryParams: "?name:contains=John&sort=invalid_field", + expectedStatus: http.StatusBadRequest, + }, + { + name: "invalid filter value", + queryParams: "?name=123", + expectedStatus: http.StatusOK, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest("GET", "/test"+tt.queryParams, nil) + assert.NoError(t, err) + + testObj := user.Model{} + + ctx := context.Background() + ctx = context.WithValue(ctx, c.FiltersCtxKey, tags.GetFilterSchema(testObj)) + + rr := httptest.NewRecorder() + handler := middleware.ListQuery(testObj)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + handler.ServeHTTP(rr, req.WithContext(ctx)) + + assert.Equal(t, tt.expectedStatus, rr.Code) + }) + } +} + +func TestGetFiltersFromContext(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + req, err := http.NewRequest("GET", "/test", nil) + assert.NoError(t, err) + + filters := []model.Filter{ + {Field: "name", Modifier: "contains", Value: []string{"test"}}, + } + ctx := context.WithValue(req.Context(), c.FiltersCtxKey, filters) + req = req.WithContext(ctx) + + result := middleware.GetFiltersFromContext(req) + assert.Equal(t, filters, result) +} + +func TestGetSortFromContext(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + req, err := http.NewRequest("GET", "/test", nil) + assert.NoError(t, err) + + sorts := []model.Sort{ + {Field: "name", Direction: "asc"}, + } + ctx := context.WithValue(req.Context(), c.SortCtxKey, sorts) + req = req.WithContext(ctx) + + result := middleware.GetSortFromContext(req) + assert.Equal(t, sorts, result) +} diff --git a/backend/internal/api/middleware/log.go b/backend/internal/api/middleware/log.go new file mode 100644 index 000000000..12e80dceb --- /dev/null +++ b/backend/internal/api/middleware/log.go @@ -0,0 +1,16 @@ +package middleware + +import ( + "net/http" + + "npm/internal/logger" +) + +// Log will print out route information to the logger +// only when debug is enabled +func Log(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger.Debug("Request: %s %s", r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/middleware/pretty_print.go b/backend/internal/api/middleware/pretty_print.go new file mode 100644 index 000000000..7baa14a26 --- /dev/null +++ b/backend/internal/api/middleware/pretty_print.go @@ -0,0 +1,22 @@ +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 := context.WithValue(r.Context(), 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 000000000..8f09e580f --- /dev/null +++ b/backend/internal/api/middleware/schema.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "context" + "encoding/json" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + + "github.com/qri-io/jsonschema" + "github.com/rotisserie/eris" +) + +// CheckRequestSchema checks the payload against schema +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, eris.Wrapf(err, "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, err.Error(), nil) + return + } + + if len(schemaErrors) > 0 { + h.ResultSchemaErrorJSON(w, r, schemaErrors) + return + } + + // All good + next.ServeHTTP(w, r) + }) + } +} diff --git a/backend/internal/api/middleware/sse_auth.go b/backend/internal/api/middleware/sse_auth.go new file mode 100644 index 000000000..9b4a508ec --- /dev/null +++ b/backend/internal/api/middleware/sse_auth.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "net/http" + + h "npm/internal/api/http" + "npm/internal/entity/user" + + "github.com/go-chi/jwtauth/v5" +) + +// SSEAuth will validate that the jwt token provided to get this far is a SSE token +// and that the user is enabled +func SSEAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + token, claims, err := jwtauth.FromContext(ctx) + + if err != nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil) + return + } + + if token == nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "No token given", nil) + return + } + + if claims == nil { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + userID := uint(claims["uid"].(float64)) + _, enabled, _ := user.IsEnabled(userID) + if !enabled { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + iss, _ := token.Get("iss") + if iss != "sse" { + h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil) + return + } + + // Should be all good now + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go new file mode 100644 index 000000000..8928d59d5 --- /dev/null +++ b/backend/internal/api/router.go @@ -0,0 +1,375 @@ +package api + +import ( + "net/http" + "time" + + "npm/internal/api/handler" + "npm/internal/api/middleware" + "npm/internal/api/schema" + "npm/internal/config" + "npm/internal/entity/accesslist" + "npm/internal/entity/certificate" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/entity/host" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/setting" + "npm/internal/entity/stream" + "npm/internal/entity/upstream" + "npm/internal/entity/user" + "npm/internal/logger" + "npm/internal/serverevents" + + "github.com/go-chi/chi/v5" + chiMiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" +) + +// NewRouter returns a new router object +func NewRouter() http.Handler { + // Cors + corss := 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), + corss.Handler, + chiMiddleware.RealIP, + chiMiddleware.Recoverer, + chiMiddleware.Throttle(5), + middleware.PrettyPrint, + middleware.Expansion, + middleware.DecodeAuth(), + middleware.BodyContext(), + middleware.Log, + ) + + return applyRoutes(r) +} + +// applyRoutes is where the magic happens +func applyRoutes(r chi.Router) chi.Router { + middleware.AuthCacheInit() + r.NotFound(handler.NotFound()) + r.MethodNotAllowed(handler.NotAllowed()) + + // OAuth endpoints aren't technically API endpoints + r.With(middleware.EnforceSetup()).Route("/oauth", func(r chi.Router) { + r.Get("/login", handler.OAuthLogin()) + r.Get("/redirect", handler.OAuthRedirect()) + }) + + // SSE - requires a sse token as the `jwt` get parameter + // Exists inside /api but it's here so that we can skip the Timeout middleware + // that applies to other endpoints. + r.With(middleware.EnforceSetup(), middleware.SSEAuth). + Mount("/api/sse", serverevents.Get()) + + // API + r.With(chiMiddleware.Timeout(30*time.Second)).Route("/api", func(r chi.Router) { + r.Get("/", handler.Health()) + r.Get("/schema", handler.Schema()) + r.With(middleware.EnforceSetup(), middleware.Enforce()). + Get("/config", handler.Config()) + + // Auth + r.With(middleware.EnforceSetup()).Route("/auth", func(r chi.Router) { + r.Get("/", handler.GetAuthConfig()) + r.With(middleware.EnforceRequestSchema(schema.GetToken())). + Post("/", handler.NewToken()) + r.With(middleware.Enforce()). + Post("/refresh", handler.RefreshToken()) + r.With(middleware.Enforce()). + Post("/sse", handler.NewSSEToken()) + }) + + // Users + r.Route("/users", func(r chi.Router) { + // Create - can be done in Setup stage as well + r.With( + middleware.Enforce(user.CapabilityUsersManage), + middleware.EnforceRequestSchema(schema.CreateUser()), + ).Post("/", handler.CreateUser()) + + // Requires Setup stage to be completed + r.With(middleware.EnforceSetup()).Route("/", func(r chi.Router) { + // Get yourself, requires a login but no other permissions + r.With(middleware.Enforce()). + Get("/{userID:me}", handler.GetUser()) + + // Update yourself, requires a login but no other permissions + r.With( + middleware.Enforce(), + middleware.EnforceRequestSchema(schema.UpdateUser()), + ).Put("/{userID:me}", handler.UpdateUser()) + + r.With(middleware.Enforce(user.CapabilityUsersManage)).Route("/", func(r chi.Router) { + // List + r.With(middleware.ListQuery(user.Model{})).Get("/", handler.GetUsers()) + + // Specific Item + r.Get("/{userID:[0-9]+}", handler.GetUser()) + r.Delete("/{userID:([0-9]+|me)}", handler.DeleteUser()) + + // Update another user + r.With(middleware.EnforceRequestSchema(schema.UpdateUser())). + Put("/{userID:[0-9]+}", handler.UpdateUser()) + }) + + // Auth - sets passwords + r.With( + middleware.Enforce(), + middleware.EnforceRequestSchema(schema.SetAuth()), + ).Post("/{userID:me}/auth", handler.SetAuth()) + r.With( + middleware.Enforce(user.CapabilityUsersManage), + middleware.EnforceRequestSchema(schema.SetAuth()), + ).Post("/{userID:[0-9]+}/auth", handler.SetAuth()) + }) + }) + + // Only available in debug mode + if config.GetLogLevel() == logger.DebugLevel { + // delete users without auth + r.Delete("/users", handler.DeleteUsers()) + // SSE test endpoints + r.Post("/sse-notification", handler.TestSSENotification()) + } + + // Settings + r.With(middleware.EnforceSetup(), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) { + // List + r.With( + middleware.ListQuery(setting.Model{}), + ).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()) + }) + + // Access Lists + r.With(middleware.EnforceSetup()).Route("/access-lists", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityAccessListsView), + middleware.ListQuery(accesslist.Model{}), + ).Get("/", handler.GetAccessLists()) + + // Create + r.With(middleware.Enforce(user.CapabilityAccessListsManage), middleware.EnforceRequestSchema(schema.CreateAccessList())). + Post("/", handler.CreateAccessList()) + + // Specific Item + r.Route("/{accessListID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityAccessListsView)). + Get("/", handler.GetAccessList()) + r.With(middleware.Enforce(user.CapabilityAccessListsManage)).Route("/", func(r chi.Router) { + r.Delete("/{accessListID:[0-9]+}", handler.DeleteAccessList()) + r.With(middleware.EnforceRequestSchema(schema.UpdateAccessList())). + Put("/{accessListID:[0-9]+}", handler.UpdateAccessList()) + }) + }) + }) + + // DNS Providers + r.With(middleware.EnforceSetup()).Route("/dns-providers", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityDNSProvidersView), + middleware.ListQuery(dnsprovider.Model{}), + ).Get("/", handler.GetDNSProviders()) + + // Create + r.With(middleware.Enforce(user.CapabilityDNSProvidersManage), middleware.EnforceRequestSchema(schema.CreateDNSProvider())). + Post("/", handler.CreateDNSProvider()) + + // Specific Item + r.Route("/{providerID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityDNSProvidersView)). + Get("/{providerID:[0-9]+}", handler.GetDNSProvider()) + r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteDNSProvider()) + r.With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())). + Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider()) + }) + }) + + // List Acme DNS Providers + r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) { + r.Get("/{acmeshID:[a-z0-9_]+}", handler.GetAcmeshProvider()) + r.Get("/", handler.GetAcmeshProviders()) + }) + }) + + // Certificate Authorities + r.With(middleware.EnforceSetup()).Route("/certificate-authorities", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityCertificateAuthoritiesView), + middleware.ListQuery(certificateauthority.Model{}), + ).Get("/", handler.GetCertificateAuthorities()) + + // Create + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage), middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())). + Post("/", handler.CreateCertificateAuthority()) + + // Specific Item + r.Route("/{caID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)). + Get("/", handler.GetCertificateAuthority()) + + r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). + Put("/", handler.UpdateCertificateAuthority()) + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)). + Delete("/", handler.DeleteCertificateAuthority()) + + r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Route("/", func(r chi.Router) { + r.Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority()) + r.With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())). + Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority()) + }) + }) + }) + + // Certificates + r.With(middleware.EnforceSetup()).Route("/certificates", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityCertificatesView), + middleware.ListQuery(certificate.Model{}), + ).Get("/", handler.GetCertificates()) + + // Create + r.With(middleware.Enforce(user.CapabilityCertificatesManage), middleware.EnforceRequestSchema(schema.CreateCertificate())). + Post("/", handler.CreateCertificate()) + + // Specific Item + r.Route("/{certificateID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityCertificatesView)). + Get("/", handler.GetCertificate()) + r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteCertificate()) + r.Put("/", handler.UpdateCertificate()) + // r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())). + // Put("/", handler.UpdateCertificate()) + r.Post("/renew", handler.RenewCertificate()) + r.Get("/download", handler.DownloadCertificate()) + }) + }) + }) + + // Hosts + r.With(middleware.EnforceSetup()).Route("/hosts", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityHostsView), + middleware.ListQuery(host.Model{}), + ).Get("/", handler.GetHosts()) + + // Create + r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateHost())). + Post("/", handler.CreateHost()) + + // Specific Item + r.Route("/{hostID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityHostsView)). + Get("/", handler.GetHost()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteHost()) + r.With(middleware.EnforceRequestSchema(schema.UpdateHost())). + Put("/", handler.UpdateHost()) + r.Get("/nginx-config", handler.GetHostNginxConfig("json")) + r.Get("/nginx-config.txt", handler.GetHostNginxConfig("text")) + }) + }) + }) + + // Nginx Templates + r.With(middleware.EnforceSetup()).Route("/nginx-templates", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityNginxTemplatesView), + middleware.ListQuery(nginxtemplate.Model{}), + ).Get("/", handler.GetNginxTemplates()) + + // Create + r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage), middleware.EnforceRequestSchema(schema.CreateNginxTemplate())). + Post("/", handler.CreateNginxTemplate()) + + // Specific Item + r.Route("/{templateID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)). + Get("/", handler.GetNginxTemplates()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteNginxTemplate()) + r.With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())). + Put("/", handler.UpdateNginxTemplate()) + }) + }) + }) + + // Streams + r.With(middleware.EnforceSetup()).Route("/streams", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityStreamsView), + middleware.ListQuery(stream.Model{}), + ).Get("/", handler.GetStreams()) + + // Create + r.With(middleware.Enforce(user.CapabilityStreamsManage), middleware.EnforceRequestSchema(schema.CreateStream())). + Post("/", handler.CreateStream()) + + // Specific Item + r.Route("/{hostID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityStreamsView)). + Get("/", handler.GetStream()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteStream()) + r.With(middleware.EnforceRequestSchema(schema.UpdateStream())). + Put("/", handler.UpdateStream()) + }) + }) + }) + + // Upstreams + r.With(middleware.EnforceSetup()).Route("/upstreams", func(r chi.Router) { + // List + r.With( + middleware.Enforce(user.CapabilityHostsView), + middleware.ListQuery(upstream.Model{}), + ).Get("/", handler.GetUpstreams()) + + // Create + r.With(middleware.Enforce(user.CapabilityHostsManage), middleware.EnforceRequestSchema(schema.CreateUpstream())). + Post("/", handler.CreateUpstream()) + + // Specific Item + r.Route("/{upstreamID:[0-9]+}", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityHostsView)). + Get("/", handler.GetUpstream()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Route("/", func(r chi.Router) { + r.Delete("/", handler.DeleteUpstream()) + r.With(middleware.EnforceRequestSchema(schema.UpdateUpstream())). + Put("/", handler.UpdateUpstream()) + r.Get("/nginx-config", handler.GetUpstreamNginxConfig("json")) + r.Get("/nginx-config.txt", handler.GetUpstreamNginxConfig("text")) + }) + }) + }) + }) + + return r +} diff --git a/backend/internal/api/router_test.go b/backend/internal/api/router_test.go new file mode 100644 index 000000000..22ac857fc --- /dev/null +++ b/backend/internal/api/router_test.go @@ -0,0 +1,56 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "npm/internal/config" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +var ( + r = NewRouter() + version = "3.0.0" + commit = "abcdefgh" +) + +// Tear up/down +func TestMain(m *testing.M) { + config.Init(&version, &commit) + code := m.Run() + os.Exit(code) +} + +func TestGetHealthz(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, + goleak.IgnoreAnyFunction("github.com/patrickmn/go-cache.(*janitor).Run"), + goleak.IgnoreAnyFunction("github.com/jc21/go-sse.(*Server).dispatch"), + ) + + 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) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, + goleak.IgnoreAnyFunction("github.com/patrickmn/go-cache.(*janitor).Run"), + goleak.IgnoreAnyFunction("github.com/jc21/go-sse.(*Server).dispatch"), + ) + + respRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/non-existent-endpoint.jpg", 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 000000000..780b02777 --- /dev/null +++ b/backend/internal/api/schema/certificates.go @@ -0,0 +1,205 @@ +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", + "additionalProperties": false, + "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", + "additionalProperties": false, + "required": [ + "type", + "certificate_authority_id", + "name", + "domain_names" + ], + "properties": { + "type": %s, + "certificate_authority_id": %s, + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + }, + "is_ecc": { + "type": "boolean" + } + } + }`, 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", + "additionalProperties": false, + "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" + }, + "is_ecc": { + "type": "boolean" + } + } + }`, 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", + "additionalProperties": false, + "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", + "additionalProperties": false, + "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", + "additionalProperties": false, + "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", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "domain_names": %s, + "meta": { + "type": "object" + } + } + }`, stringMinMax(1, 100), domainNames()) +} + +func updateCertificateMkcert() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "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 000000000..af02f8eab --- /dev/null +++ b/backend/internal/api/schema/common.go @@ -0,0 +1,73 @@ +package schema + +import "fmt" + +func strictString(value string) string { + return fmt.Sprintf(`{ + "type": "string", + "pattern": "^%s$" + }`, value) +} + +const intMinOne = ` +{ + "type": "integer", + "minimum": 1 +} +` + +const boolean = ` +{ + "type": "boolean" +} +` + +func stringMinMax(minLength, maxLength int) string { + return fmt.Sprintf(`{ + "type": "string", + "minLength": %d, + "maxLength": %d + }`, minLength, maxLength) +} + +func capabilties() string { + return `{ + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }` +} + +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" + }, + { + "type": "string" + } + ] +} +` diff --git a/backend/internal/api/schema/create_access_list.go b/backend/internal/api/schema/create_access_list.go new file mode 100644 index 000000000..47ded39df --- /dev/null +++ b/backend/internal/api/schema/create_access_list.go @@ -0,0 +1,21 @@ +package schema + +import ( + "fmt" +) + +// CreateAccessList is the schema for incoming data validation +func CreateAccessList() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": %s + } + } + `, stringMinMax(2, 100)) +} 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 000000000..113c2ce36 --- /dev/null +++ b/backend/internal/api/schema/create_certificate_authority.go @@ -0,0 +1,25 @@ +package schema + +import "fmt" + +// CreateCertificateAuthority is the schema for incoming data validation +func CreateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "acmesh_server", + "max_domains" + ], + "properties": { + "name": %s, + "acmesh_server": %s, + "max_domains": %s, + "ca_bundle": %s, + "is_wildcard_supported": %s + } + } + `, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean) +} 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 000000000..d51f2dcc0 --- /dev/null +++ b/backend/internal/api/schema/create_dns_provider.go @@ -0,0 +1,59 @@ +package schema + +import ( + "fmt" + "strings" + + "npm/internal/dnsproviders" + "npm/internal/logger" + "npm/internal/util" + + "github.com/rotisserie/eris" +) + +// CreateDNSProvider is the schema for incoming data validation +func CreateDNSProvider() string { + allProviders := dnsproviders.GetAll() + fmtStr := fmt.Sprintf(`{"oneOf": [%s]}`, strings.TrimRight(strings.Repeat("\n%s,", len(allProviders)), ",")) + + allSchemasWrapped := make([]string, 0) + for providerName, provider := range allProviders { + schema, err := provider.GetJSONSchema() + if err != nil { + logger.Error("ProviderSchemaError", eris.Wrapf(err, "Invalid Provider Schema for %s: %v", provider.Title, err)) + } else { + allSchemasWrapped = append(allSchemasWrapped, createDNSProviderType(providerName, schema)) + } + } + + return fmt.Sprintf(fmtStr, util.ConvertStringSliceToInterface(allSchemasWrapped)...) +} + +func createDNSProviderType(name, metaSchema string) string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "acmesh_name", + "name", + "meta" + ], + "properties": { + "acmesh_name": { + "type": "string", + "pattern": "^%s$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "dns_sleep": { + "type": "integer" + }, + "meta": %s + } + } + `, name, metaSchema) +} diff --git a/backend/internal/api/schema/create_host.go b/backend/internal/api/schema/create_host.go new file mode 100644 index 000000000..819d97643 --- /dev/null +++ b/backend/internal/api/schema/create_host.go @@ -0,0 +1,88 @@ +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", + "nginx_template_id", + "proxy_scheme" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^proxy$" + }, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "listen_interface": %s, + "domain_names": %s, + "upstream_id": { + "type": "integer" + }, + "proxy_scheme": { + "type": "string", + "pattern": "^https?$" + }, + "proxy_host": { + "type": "string" + }, + "proxy_port": { + "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" + }, + "advanced_config": { + "type": "string" + }, + "is_disabled": { + "type": "boolean" + } + } + } + ] + } + `, stringMinMax(0, 255), domainNames()) +} diff --git a/backend/internal/api/schema/create_nginx_template.go b/backend/internal/api/schema/create_nginx_template.go new file mode 100644 index 000000000..7fe3fc6f8 --- /dev/null +++ b/backend/internal/api/schema/create_nginx_template.go @@ -0,0 +1,30 @@ +package schema + +// CreateNginxTemplate is the schema for incoming data validation +func CreateNginxTemplate() string { + return ` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "type", + "template" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "pattern": "^proxy|redirect|dead|stream|upstream$" + }, + "template": { + "type": "string", + "minLength": 20 + } + } + } + ` +} diff --git a/backend/internal/api/schema/create_setting.go b/backend/internal/api/schema/create_setting.go new file mode 100644 index 000000000..dca3869c0 --- /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 000000000..792b8818e --- /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_upstream.go b/backend/internal/api/schema/create_upstream.go new file mode 100644 index 000000000..3f2be4648 --- /dev/null +++ b/backend/internal/api/schema/create_upstream.go @@ -0,0 +1,73 @@ +package schema + +import "fmt" + +// CreateUpstream is the schema for incoming data validation +func CreateUpstream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "servers", + "nginx_template_id" + ], + "properties": { + "name": %s, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "advanced_config": %s, + "ip_hash": { + "type": "boolean" + }, + "ntlm": { + "type": "boolean" + }, + "keepalive": { + "type": "integer" + }, + "keepalive_requests": { + "type": "integer" + }, + "keepalive_time": { + "type": "string" + }, + "keepalive_timeout": { + "type": "string" + }, + "servers": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "server" + ], + "properties": { + "server": %s, + "weight": { + "type": "integer" + }, + "max_conns": { + "type": "integer" + }, + "max_fails": { + "type": "integer" + }, + "fail_timeout": { + "type": "integer" + }, + "backup": { + "type": "boolean" + } + } + } + } + } + } +`, stringMinMax(1, 100), stringMinMax(0, 1024), stringMinMax(2, 255)) +} diff --git a/backend/internal/api/schema/create_user.go b/backend/internal/api/schema/create_user.go new file mode 100644 index 000000000..3ad1346f7 --- /dev/null +++ b/backend/internal/api/schema/create_user.go @@ -0,0 +1,41 @@ +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", + "is_disabled", + "capabilities" + ], + "properties": { + "name": %s, + "email": %s, + "is_disabled": { + "type": "boolean" + }, + "auth": { + "type": "object", + "required": [ + "type", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^local$" + }, + "secret": %s + } + }, + "capabilities": %s + } + } + `, stringMinMax(2, 50), stringMinMax(5, 150), stringMinMax(8, 255), capabilties()) +} diff --git a/backend/internal/api/schema/get_token.go b/backend/internal/api/schema/get_token.go new file mode 100644 index 000000000..142928924 --- /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", + "enum": ["local", "ldap"] + }, + "identity": %s, + "secret": %s + } + } + `, stdField, stdField) +} diff --git a/backend/internal/api/schema/schema_test.go b/backend/internal/api/schema/schema_test.go new file mode 100644 index 000000000..4b69b390c --- /dev/null +++ b/backend/internal/api/schema/schema_test.go @@ -0,0 +1,133 @@ +package schema + +import ( + "bytes" + "encoding/json" + "testing" + + "npm/internal/entity/certificate" + + "github.com/stretchr/testify/assert" +) + +func TestSchemas(t *testing.T) { + tests := []struct { + name string + schema string + }{ + { + name: "CreateCertificate", + schema: CreateCertificate(), + }, + { + name: "UpdateCertificate TypeHTTP", + schema: UpdateCertificate(certificate.TypeHTTP), + }, + { + name: "UpdateCertificate TypeDNS", + schema: UpdateCertificate(certificate.TypeDNS), + }, + { + name: "UpdateCertificate TypeCustom", + schema: UpdateCertificate(certificate.TypeCustom), + }, + { + name: "UpdateCertificate TypeMkcert", + schema: UpdateCertificate(certificate.TypeMkcert), + }, + { + name: "UpdateCertificate default", + schema: UpdateCertificate(""), + }, + { + name: "CreateAccessList", + schema: CreateAccessList(), + }, + { + name: "CreateCertificateAuthority", + schema: CreateCertificateAuthority(), + }, + { + name: "CreateDNSProvider", + schema: CreateDNSProvider(), + }, + { + name: "CreateHost", + schema: CreateHost(), + }, + { + name: "CreateNginxTemplate", + schema: CreateNginxTemplate(), + }, + { + name: "CreateSetting", + schema: CreateSetting(), + }, + { + name: "CreateStream", + schema: CreateStream(), + }, + { + name: "CreateUpstream", + schema: CreateUpstream(), + }, + { + name: "CreateUser", + schema: CreateUser(), + }, + { + name: "GetToken", + schema: GetToken(), + }, + { + name: "SetAuth", + schema: SetAuth(), + }, + { + name: "UpdateAccessList", + schema: UpdateAccessList(), + }, + { + name: "UpdateCertificateAuthority", + schema: UpdateCertificateAuthority(), + }, + { + name: "UpdateDNSProvider", + schema: UpdateDNSProvider(), + }, + { + name: "UpdateHost", + schema: UpdateHost(), + }, + { + name: "UpdateNginxTemplate", + schema: UpdateNginxTemplate(), + }, + { + name: "UpdateSetting", + schema: UpdateSetting(), + }, + { + name: "UpdateStream", + schema: UpdateStream(), + }, + { + name: "UpdateUpstream", + schema: UpdateUpstream(), + }, + { + name: "UpdateUser", + schema: UpdateUser(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + byt := []byte(tt.schema) + var prettyJSON bytes.Buffer + err := json.Indent(&prettyJSON, byt, "", " ") + assert.NoError(t, err) + assert.Greater(t, len(prettyJSON.String()), 0) + }) + } +} diff --git a/backend/internal/api/schema/set_auth.go b/backend/internal/api/schema/set_auth.go new file mode 100644 index 000000000..2ffdb605e --- /dev/null +++ b/backend/internal/api/schema/set_auth.go @@ -0,0 +1,26 @@ +package schema + +import "fmt" + +// SetAuth is the schema for incoming data validation +// Only local auth is supported for setting a password +func SetAuth() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "secret" + ], + "properties": { + "type": { + "type": "string", + "pattern": "^local$" + }, + "secret": %s, + "current_secret": %s + } + } + `, stringMinMax(8, 225), stringMinMax(8, 225)) +} diff --git a/backend/internal/api/schema/update_access_list.go b/backend/internal/api/schema/update_access_list.go new file mode 100644 index 000000000..786ac26bf --- /dev/null +++ b/backend/internal/api/schema/update_access_list.go @@ -0,0 +1,17 @@ +package schema + +import "fmt" + +// UpdateAccessList is the schema for incoming data validation +func UpdateAccessList() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s + } + } + `, stringMinMax(2, 100)) +} 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 000000000..e53db5c26 --- /dev/null +++ b/backend/internal/api/schema/update_certificate_authority.go @@ -0,0 +1,21 @@ +package schema + +import "fmt" + +// UpdateCertificateAuthority is the schema for incoming data validation +func UpdateCertificateAuthority() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "acmesh_server": %s, + "max_domains": %s, + "ca_bundle": %s, + "is_wildcard_supported": %s + } + } + `, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean) +} 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 000000000..b852c88ac --- /dev/null +++ b/backend/internal/api/schema/update_dns_provider.go @@ -0,0 +1,20 @@ +package schema + +import "fmt" + +// UpdateDNSProvider is the schema for incoming data validation +func UpdateDNSProvider() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "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 000000000..21643bbea --- /dev/null +++ b/backend/internal/api/schema/update_host.go @@ -0,0 +1,27 @@ +package schema + +import "fmt" + +// UpdateHost is the schema for incoming data validation +func UpdateHost() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "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_nginx_template.go b/backend/internal/api/schema/update_nginx_template.go new file mode 100644 index 000000000..8f8e14590 --- /dev/null +++ b/backend/internal/api/schema/update_nginx_template.go @@ -0,0 +1,22 @@ +package schema + +// UpdateNginxTemplate is the schema for incoming data validation +func UpdateNginxTemplate() string { + return ` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "template": { + "type": "string", + "minLength": 20 + } + } + } + ` +} diff --git a/backend/internal/api/schema/update_setting.go b/backend/internal/api/schema/update_setting.go new file mode 100644 index 000000000..e9af221bb --- /dev/null +++ b/backend/internal/api/schema/update_setting.go @@ -0,0 +1,17 @@ +package schema + +import "fmt" + +// UpdateSetting is the schema for incoming data validation +func UpdateSetting() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "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 000000000..51d85ff61 --- /dev/null +++ b/backend/internal/api/schema/update_stream.go @@ -0,0 +1,23 @@ +package schema + +import "fmt" + +// UpdateStream is the schema for incoming data validation +func UpdateStream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "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_upstream.go b/backend/internal/api/schema/update_upstream.go new file mode 100644 index 000000000..c259f4208 --- /dev/null +++ b/backend/internal/api/schema/update_upstream.go @@ -0,0 +1,69 @@ +package schema + +import "fmt" + +// UpdateUpstream is the schema for incoming data validation +func UpdateUpstream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": %s, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "advanced_config": %s, + "ip_hash": { + "type": "boolean" + }, + "ntlm": { + "type": "boolean" + }, + "keepalive": { + "type": "integer" + }, + "keepalive_requests": { + "type": "integer" + }, + "keepalive_time": { + "type": "string" + }, + "keepalive_timeout": { + "type": "string" + }, + "servers": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "server" + ], + "properties": { + "server": %s, + "weight": { + "type": "integer" + }, + "max_conns": { + "type": "integer" + }, + "max_fails": { + "type": "integer" + }, + "fail_timeout": { + "type": "integer" + }, + "backup": { + "type": "boolean" + } + } + } + } + } + } +`, stringMinMax(1, 100), stringMinMax(0, 1024), stringMinMax(2, 255)) +} diff --git a/backend/internal/api/schema/update_user.go b/backend/internal/api/schema/update_user.go new file mode 100644 index 000000000..df4815bf4 --- /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, + "minProperties": 1, + "properties": { + "name": %s, + "email": %s, + "is_disabled": { + "type": "boolean" + }, + "capabilities": %s + } + } + `, stringMinMax(2, 50), stringMinMax(5, 150), capabilties()) +} diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go new file mode 100644 index 000000000..f6593df4e --- /dev/null +++ b/backend/internal/api/server.go @@ -0,0 +1,30 @@ +package api + +import ( + "fmt" + "net/http" + "time" + + "npm/internal/logger" + "npm/internal/serverevents" +) + +const httpPort = 3000 + +// StartServer creates a http server +func StartServer() { + logger.Info("Server starting on port %v", httpPort) + + server := &http.Server{ + Addr: fmt.Sprintf(":%v", httpPort), + Handler: NewRouter(), + ReadHeaderTimeout: 3 * time.Second, + } + + defer serverevents.Shutdown() + + err := server.ListenAndServe() + 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 422b4f467..000000000 --- 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/certificate.js b/backend/internal/certificate.js deleted file mode 100644 index 7c8fddeea..000000000 --- a/backend/internal/certificate.js +++ /dev/null @@ -1,1223 +0,0 @@ -const _ = require('lodash'); -const fs = require('fs'); -const https = require('https'); -const tempWrite = require('temp-write'); -const moment = require('moment'); -const logger = require('../logger').ssl; -const error = require('../lib/error'); -const utils = require('../lib/utils'); -const certificateModel = require('../models/certificate'); -const dnsPlugins = require('../global/certbot-dns-plugins'); -const internalAuditLog = require('./audit-log'); -const internalNginx = require('./nginx'); -const internalHost = require('./host'); -const letsencryptStaging = process.env.NODE_ENV !== 'production'; -const letsencryptConfig = '/etc/letsencrypt.ini'; -const certbotCommand = 'certbot'; -const archiver = require('archiver'); -const path = require('path'); -const { isArray } = require('lodash'); - -function omissions() { - return ['is_deleted']; -} - -const internalCertificate = { - - allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], - intervalTimeout: 1000 * 60 * 60, // 1 hour - interval: null, - intervalProcessing: false, - - initTimer: () => { - logger.info('Let\'s Encrypt Renewal Timer initialized'); - internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.intervalTimeout); - // 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.intervalProcessing) { - internalCertificate.intervalProcessing = true; - logger.info('Renewing SSL certs close to expiry...'); - - const cmd = certbotCommand + ' renew --non-interactive --quiet ' + - '--config "' + letsencryptConfig + '" ' + - '--preferred-challenges "dns,http" ' + - '--disable-hook-validation ' + - (letsencryptStaging ? '--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.intervalProcessing = false; - }) - .catch((err) => { - logger.error(err); - internalCertificate.intervalProcessing = 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.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(async() => await new Promise((r) => setTimeout(r, 5000))) - .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 - * @returns {Promise} - */ - download: (access, data) => { - return new Promise((resolve, reject) => { - access.can('certificates:get', data) - .then(() => { - return internalCertificate.get(access, data); - }) - .then((certificate) => { - if (certificate.provider === 'letsencrypt') { - const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id; - - if (!fs.existsSync(zipDirectory)) { - throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists'); - } - - let certFiles = fs.readdirSync(zipDirectory) - .filter((fn) => fn.endsWith('.pem')) - .map((fn) => fs.realpathSync(path.join(zipDirectory, fn))); - const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`; - const opName = '/tmp/' + downloadName; - internalCertificate.zipFiles(certFiles, opName) - .then(() => { - logger.debug('zip completed : ', opName); - const resp = { - fileName: opName - }; - resolve(resp); - }).catch((err) => reject(err)); - } else { - throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded'); - } - }).catch((err) => reject(err)); - }); - }, - - /** - * @param {String} source - * @param {String} out - * @returns {Promise} - */ - zipFiles(source, out) { - const archive = archiver('zip', { zlib: { level: 9 } }); - const stream = fs.createWriteStream(out); - - return new Promise((resolve, reject) => { - source - .map((fl) => { - let fileName = path.basename(fl); - logger.debug(fl, 'added to certificate zip'); - archive.file(fl, { name: fileName }); - }); - archive - .on('error', (err) => reject(err)) - .pipe(stream); - - stream.on('close', () => resolve()); - archive.finalize(); - }); - }, - - /** - * @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('nice_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) => { - logger.info('Writing Custom Certificate:', certificate); - - const 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 certData = certificate.meta.certificate; - if (typeof certificate.meta.intermediate_certificate !== 'undefined') { - certData = certData + '\n' + certificate.meta.intermediate_certificate; - } - - try { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - } catch (err) { - reject(err); - return; - } - - fs.writeFile(dir + '/fullchain.pem', certData, 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.allowedSslFiles.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.allowedSslFiles.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.allowedSslFiles); - }); - }); - }, - - /** - * 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((certData) => { - fs.unlinkSync(filepath); - return certData; - }).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 certData = {}; - - return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout') - .then((result) => { - // subject=CN = something.example.com - const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; - const match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine subject from certificate: ' + result); - } - - certData['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 - const regex = /^(?:issuer=)?(.*)$/gim; - const match = regex.exec(result); - - if (typeof match[1] === 'undefined') { - throw new error.ValidationError('Could not determine issuer from certificate: ' + result); - } - - certData['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 validFrom = null; - let validTo = null; - - const lines = result.split('\n'); - lines.map(function (str) { - const regex = /^(\S+)=(.*)$/gim; - const match = regex.exec(str.trim()); - - if (match && typeof match[2] !== 'undefined') { - const date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10); - - if (match[1].toLowerCase() === 'notbefore') { - validFrom = date; - } else if (match[1].toLowerCase() === 'notafter') { - validTo = date; - } - } - }); - - if (!validFrom || !validTo) { - throw new error.ValidationError('Could not determine dates from certificate: ' + result); - } - - if (throw_expired && validTo < parseInt(moment().format('X'), 10)) { - throw new error.ValidationError('Certificate has expired'); - } - - certData['dates'] = { - from: validFrom, - to: validTo - }; - - return certData; - }).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.allowedSslFiles.map((key) => { - if (typeof meta[key] !== 'undefined' && meta[key]) { - if (remove) { - delete meta[key]; - } else { - meta[key] = true; - } - } - }); - - return meta; - }, - - /** - * Request a certificate using the http challenge - * @param {Object} certificate the certificate row - * @returns {Promise} - */ - requestLetsEncryptSsl: (certificate) => { - logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', ')); - - const cmd = certbotCommand + ' certonly ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--authenticator webroot ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--preferred-challenges "dns,http" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - (letsencryptStaging ? '--staging' : ''); - - 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 = dnsPlugins[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 credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - // Escape single quotes and backslashes - const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); - const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\''; - const prepareCmd = 'pip install ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies; - - // Whether the plugin has a ---credentials argument - const hasConfigArg = certificate.meta.dns_provider !== 'route53'; - - let mainCmd = certbotCommand + ' certonly ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--agree-tos ' + - '--email "' + certificate.meta.letsencrypt_email + '" ' + - '--domains "' + certificate.domain_names.join(',') + '" ' + - '--authenticator ' + dns_plugin.full_plugin_name + ' ' + - ( - hasConfigArg - ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"' - : '' - ) + - ( - certificate.meta.propagation_seconds !== undefined - ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds - : '' - ) + - (letsencryptStaging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; - } - - logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`); - - return utils.exec(credentialsCmd) - .then(() => { - return utils.exec(prepareCmd) - .then(() => { - return utils.exec(mainCmd) - .then(async (result) => { - logger.info(result); - return result; - }); - }); - }).catch(async (err) => { - // Don't fail if file does not exist - const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`; - await utils.exec(delete_credentialsCmd); - 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') { - const 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(', ')); - - const cmd = certbotCommand + ' renew --force-renewal ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--preferred-challenges "dns,http" ' + - '--no-random-sleep-on-renew ' + - '--disable-hook-validation ' + - (letsencryptStaging ? '--staging' : ''); - - 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 = dnsPlugins[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 mainCmd = certbotCommand + ' renew ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-name "npm-' + certificate.id + '" ' + - '--disable-hook-validation ' + - '--no-random-sleep-on-renew ' + - (letsencryptStaging ? ' --staging' : ''); - - // Prepend the path to the credentials file as an environment variable - if (certificate.meta.dns_provider === 'route53') { - const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; - mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; - } - - logger.info('Command:', mainCmd); - - return utils.exec(mainCmd) - .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 mainCmd = certbotCommand + ' revoke ' + - '--config "' + letsencryptConfig + '" ' + - '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' + - '--delete-after-revoke ' + - (letsencryptStaging ? '--staging' : ''); - - // Don't fail command if file does not exist - const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`; - - logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd); - - return utils.exec(mainCmd) - .then(async (result) => { - await utils.exec(delete_credentialsCmd); - logger.info(result); - return result; - }) - .catch((err) => { - logger.error(err.message); - - if (throw_errors) { - throw err; - } - }); - }, - - /** - * @param {Object} certificate - * @returns {Boolean} - */ - hasLetsEncryptSslCerts: (certificate) => { - const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id; - - return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/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(); - } - }, - - testHttpsChallenge: async (access, domains) => { - await access.can('certificates:list'); - - if (!isArray(domains)) { - throw new error.InternalValidationError('Domains must be an array of strings'); - } - if (domains.length === 0) { - throw new error.InternalValidationError('No domains provided'); - } - - // Create a test challenge file - const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge'; - const testChallengeFile = testChallengeDir + '/test-challenge'; - fs.mkdirSync(testChallengeDir, {recursive: true}); - fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'}); - - async function performTestForDomain (domain) { - logger.info('Testing http challenge for ' + domain); - const url = `http://${domain}/.well-known/acme-challenge/test-challenge`; - const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`; - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(formBody) - } - }; - - const result = await new Promise((resolve) => { - - const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) { - let responseBody = ''; - - res.on('data', (chunk) => responseBody = responseBody + chunk); - res.on('end', function () { - const parsedBody = JSON.parse(responseBody + ''); - if (res.statusCode !== 200) { - logger.warn(`Failed to test HTTP challenge for domain ${domain}`, res); - resolve(undefined); - } - resolve(parsedBody); - }); - }); - - // Make sure to write the request body. - req.write(formBody); - req.end(); - req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e); - resolve(undefined); }); - }); - - if (!result) { - // Some error occurred while trying to get the data - return 'failed'; - } else if (`${result.responsecode}` === '200' && result.htmlresponse === 'Success') { - // Server exists and has responded with the correct data - return 'ok'; - } else if (`${result.responsecode}` === '200') { - // Server exists but has responded with wrong data - logger.info(`HTTP challenge test failed for domain ${domain} because of invalid returned data:`, result.htmlresponse); - return 'wrong-data'; - } else if (`${result.responsecode}` === '404') { - // Server exists but responded with a 404 - logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`); - return '404'; - } else if (`${result.responsecode}` === '0' || (typeof result.reason === 'string' && result.reason.toLowerCase() === 'host unavailable')) { - // Server does not exist at domain - logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`); - return 'no-host'; - } else { - // Other errors - logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); - return `other:${result.responsecode}`; - } - } - - const results = {}; - - for (const domain of domains){ - results[domain] = await performTestForDomain(domain); - } - - // Remove the test challenge file - fs.unlinkSync(testChallengeFile); - - return results; - } -}; - -module.exports = internalCertificate; diff --git a/backend/internal/config/args.go b/backend/internal/config/args.go new file mode 100644 index 000000000..dffb3131a --- /dev/null +++ b/backend/internal/config/args.go @@ -0,0 +1,29 @@ +package config + +import ( + "fmt" + "os" + + "github.com/alexflint/go-arg" +) + +// ArgConfig is the settings for passing arguments to the command +type ArgConfig struct { + Version bool `arg:"-v" help:"print version and exit"` +} + +var ( + appArguments ArgConfig +) + +// InitArgs will parse arg vars +func InitArgs(version, commit *string) { + // nolint: errcheck, gosec + arg.MustParse(&appArguments) + + if appArguments.Version { + fmt.Printf("v%s (%s)\n", *version, *commit) + // nolint: revive + os.Exit(0) + } +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 000000000..11c39ad96 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,61 @@ +package config + +import ( + "fmt" + golog "log" + + "npm/internal/logger" + + "github.com/vrischmann/envconfig" +) + +// Init will parse environment variables into the Env struct +func Init(version, commit *string) { + Version = *version + Commit = *commit + + if err := envconfig.InitWithPrefix(&Configuration, "NPM"); err != nil { + fmt.Printf("%+v\n", err) + } + + if err := initLogger(); err != nil { + logger.Error("LoggerConfigurationError", err) + } +} + +// InitIPRanges will initialise the config for the ipranges command +func InitIPRanges(version, commit *string) error { + Version = *version + Commit = *commit + err := envconfig.InitWithPrefix(&Configuration, "NPM") + // nolint: errcheck, gosec + initLogger() + return err +} + +// Init initialises the Log object and return it +func initLogger() error { + // 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 + } + + return logger.Configure(&logger.Config{ + LogThreshold: logLevel, + Formatter: Configuration.Log.Format, + }) +} + +// GetLogLevel returns the logger const level +func GetLogLevel() logger.Level { + return logLevel +} diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 000000000..542d22817 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,140 @@ +package config + +import ( + "os" + "strings" + "testing" + + "npm/internal/logger" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestInit(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Setenv("NPM_DATA_FOLDER", "/path/to/some/data/folder") + t.Setenv("NPM_LOG_LEVEL", "warn") + t.Setenv("NPM_DB_DRIVER", "postgres") + t.Setenv("NPM_DB_HOST", "1.1.1.1") + t.Setenv("NPM_DB_PORT", "5432") + t.Setenv("NPM_DB_USERNAME", "rootuser") + t.Setenv("NPM_DB_PASSWORD", "4metoremember") + t.Setenv("NPM_DB_NAME", "npm") + t.Setenv("NPM_DISABLE_IPV4", "false") + t.Setenv("NPM_DISABLE_IPV6", "true") + + version := "999.999.999" + commit := "abcd124" + Init(&version, &commit) + err := InitIPRanges(&version, &commit) + assert.Nil(t, err) + + assert.Equal(t, "/path/to/some/data/folder", Configuration.DataFolder) + assert.Equal(t, false, Configuration.DisableIPV4) + assert.Equal(t, true, Configuration.DisableIPV6) + assert.Equal(t, "/data/.acme.sh", Configuration.Acmesh.Home) + assert.Equal(t, "disable", Configuration.DB.SSLMode) + assert.Equal(t, logger.WarnLevel, logger.GetLogLevel()) + + assert.Equal(t, "postgres", Configuration.DB.Driver) + assert.Equal(t, "1.1.1.1", Configuration.DB.Host) + assert.Equal(t, 5432, Configuration.DB.Port) + assert.Equal(t, "rootuser", Configuration.DB.Username) + assert.Equal(t, "4metoremember", Configuration.DB.Password) + assert.Equal(t, "npm", Configuration.DB.Name) + assert.Equal(t, "postgres", Configuration.DB.GetDriver()) +} + +func TestConnectURLs(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type want struct { + gorm string + dbmate string + } + + tests := []struct { + name string + envs []string + want want + }{ + { + name: "sqlite", + envs: []string{ + "NPM_DB_DRIVER=sqlite", + "NPM_DATA_FOLDER=/path/to/data", + }, + want: want{ + gorm: "/path/to/data/nginxproxymanager.db", + dbmate: "sqlite:/path/to/data/nginxproxymanager.db", + }, + }, + { + name: "postgres", + envs: []string{ + "NPM_DB_DRIVER=postgres", + "NPM_DB_HOST=2.2.2.2", + "NPM_DB_PORT=9824", + "NPM_DB_USERNAME=postgresuser", + "NPM_DB_PASSWORD=pgpass", + "NPM_DB_SSLMODE=strict", + "NPM_DB_NAME=npm", + }, + want: want{ + gorm: "host=2.2.2.2 user=postgresuser password=pgpass dbname=npm port=9824 sslmode=strict TimeZone=UTC", + dbmate: "postgres://postgresuser:pgpass@2.2.2.2:9824/npm?sslmode=strict", + }, + }, + { + name: "mysql", + envs: []string{ + "NPM_DB_DRIVER=mysql", + "NPM_DB_HOST=3.3.3.3", + "NPM_DB_PORT=3307", + "NPM_DB_USERNAME=mysqluser", + "NPM_DB_PASSWORD=mypass", + "NPM_DB_NAME=npm", + }, + want: want{ + gorm: "mysqluser:mypass@tcp(3.3.3.3:3307)/npm?charset=utf8mb4&parseTime=True&loc=Local", + dbmate: "mysql://mysqluser:mypass@3.3.3.3:3307/npm", + }, + }, + } + + version := "888.888.888" + commit := "abcd125" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, env := range tt.envs { + parts := strings.Split(env, "=") + if len(parts) == 2 { + t.Setenv(parts[0], parts[1]) + } + } + Init(&version, &commit) + assert.Equal(t, tt.want.gorm, Configuration.DB.GetGormConnectURL()) + assert.Equal(t, tt.want.dbmate, Configuration.DB.GetDBMateConnectURL()) + }) + } +} + +func TestCreateDataFolders(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + t.Setenv("NPM_DATA_FOLDER", "/tmp/npmtest") + + version := "777.777.777" + commit := "abcd123" + Init(&version, &commit) + CreateDataFolders() + + _, err := os.Stat("/tmp/npmtest/nginx/hosts") + assert.Nil(t, err) +} diff --git a/backend/internal/config/db.go b/backend/internal/config/db.go new file mode 100644 index 000000000..0b0832718 --- /dev/null +++ b/backend/internal/config/db.go @@ -0,0 +1,79 @@ +package config + +import ( + "fmt" + "strings" +) + +const ( + DatabaseSqlite = "sqlite" + DatabasePostgres = "postgres" + DatabaseMysql = "mysql" +) + +type db struct { + Driver string `json:"driver" envconfig:"optional,default=sqlite"` + Host string `json:"host" envconfig:"optional,default="` + Port int `json:"port" envconfig:"optional,default="` + Username string `json:"username" envconfig:"optional,default="` + Password string `json:"password" envconfig:"optional,default="` + Name string `json:"name" envconfig:"optional,default="` + SSLMode string `json:"sslmode" envconfig:"optional,default=disable"` +} + +// GetDriver returns the lowercase driver name +func (d *db) GetDriver() string { + return strings.ToLower(d.Driver) +} + +// GetGormConnectURL is used by Gorm +func (d *db) GetGormConnectURL() string { + switch d.GetDriver() { + case DatabaseSqlite: + return fmt.Sprintf("%s/nginxproxymanager.db", Configuration.DataFolder) + case DatabasePostgres: + return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=UTC", + d.Host, + d.Username, + d.Password, + d.Name, + d.Port, + d.SSLMode, + ) + case DatabaseMysql: + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + d.Username, + d.Password, + d.Host, + d.Port, + d.Name, + ) + } + return "" +} + +// GetDBMateConnectURL is used by Dbmate +func (d *db) GetDBMateConnectURL() string { + switch d.GetDriver() { + case DatabaseSqlite: + return fmt.Sprintf("sqlite:%s/nginxproxymanager.db", Configuration.DataFolder) + case DatabasePostgres: + return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", + d.Username, + d.Password, + d.Host, + d.Port, + d.Name, + d.SSLMode, + ) + case DatabaseMysql: + return fmt.Sprintf("mysql://%s:%s@%s:%d/%s", + d.Username, + d.Password, + d.Host, + d.Port, + d.Name, + ) + } + return "" +} diff --git a/backend/internal/config/folders.go b/backend/internal/config/folders.go new file mode 100644 index 000000000..1268910e6 --- /dev/null +++ b/backend/internal/config/folders.go @@ -0,0 +1,37 @@ +package config + +import ( + "fmt" + "os" + + "npm/internal/logger" +) + +// CreateDataFolders will recursively create these folders within the +// data folder defined in configuration. +func CreateDataFolders() { + folders := []string{ + "access", + "certificates", + "logs", + // Acme.sh: + Configuration.Acmesh.GetWellknown(), + // Nginx: + "nginx/hosts", + "nginx/streams", + "nginx/temp", + "nginx/upstreams", + } + + for _, folder := range folders { + path := folder + if path[0:1] != "/" { + path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder) + } + logger.Debug("Creating folder: %s", path) + // nolint: gosec + if err := os.MkdirAll(path, os.ModePerm); err != nil { + logger.Error("CreateDataFolderError", err) + } + } +} diff --git a/backend/internal/config/vars.go b/backend/internal/config/vars.go new file mode 100644 index 000000000..8d243ecb6 --- /dev/null +++ b/backend/internal/config/vars.go @@ -0,0 +1,44 @@ +package config + +import ( + "fmt" + + "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 + +var logLevel logger.Level + +// Configuration is the main configuration object +var Configuration struct { + DataFolder string `json:"data_folder" envconfig:"optional,default=/data"` + DisableIPV4 bool `json:"disable_ipv4" envconfig:"optional"` + DisableIPV6 bool `json:"disable_ipv6" envconfig:"optional"` + Acmesh acmesh `json:"acmesh"` + DB db `json:"db"` + Log log `json:"log"` +} + +type log struct { + Level string `json:"level" envconfig:"optional,default=info"` + Format string `json:"format" envconfig:"optional,default=nice"` +} + +type acmesh struct { + Home string `json:"home" envconfig:"optional,default=/data/.acme.sh"` + ConfigHome string `json:"config_home" envconfig:"optional,default=/data/.acme.sh/config"` + CertHome string `json:"cert_home" envconfig:"optional,default=/data/.acme.sh/certs"` +} + +// GetWellknown returns the well known path +func (a *acmesh) GetWellknown() string { + return fmt.Sprintf("%s/.well-known", a.Home) +} diff --git a/backend/internal/config/vars_test.go b/backend/internal/config/vars_test.go new file mode 100644 index 000000000..be3ae00de --- /dev/null +++ b/backend/internal/config/vars_test.go @@ -0,0 +1,18 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestAcmeshGetWellknown(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + a := acmesh{ + Home: "/data/.acme.sh", + } + assert.Equal(t, "/data/.acme.sh/.well-known", a.GetWellknown()) +} diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go new file mode 100644 index 000000000..7f4d429ad --- /dev/null +++ b/backend/internal/database/db.go @@ -0,0 +1,80 @@ +package database + +import ( + "fmt" + "strings" + + "npm/internal/config" + "npm/internal/logger" + + "github.com/glebarez/sqlite" + "github.com/rotisserie/eris" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" + gormlogger "gorm.io/gorm/logger" + "gorm.io/gorm/schema" +) + +var dbInstance *gorm.DB + +// NewDB creates a new connection +func NewDB() { + logger.Info("Creating new DB instance using %s", strings.ToLower(config.Configuration.DB.Driver)) + db, err := connect() + if err != nil { + logger.Error("DatabaseConnectError", err) + } else if db != nil { + dbInstance = db + } +} + +// GetDB returns an existing or new instance +func GetDB() *gorm.DB { + if dbInstance == nil { + NewDB() + } + return dbInstance +} + +// SetDB will set the dbInstance to this +// Used by unit testing to set the db to a mock database +func SetDB(db *gorm.DB) { + dbInstance = db +} + +func connect() (*gorm.DB, error) { + var d gorm.Dialector + dsn := config.Configuration.DB.GetGormConnectURL() + + switch strings.ToLower(config.Configuration.DB.Driver) { + case config.DatabaseSqlite: + // autocreate(dsn) + d = sqlite.Open(dsn) + + case config.DatabasePostgres: + d = postgres.Open(dsn) + + case config.DatabaseMysql: + d = mysql.Open(dsn) + + default: + return nil, eris.New(fmt.Sprintf("Database driver %s is not supported. Valid options are: %s, %s or %s", config.Configuration.DB.Driver, config.DatabaseSqlite, config.DatabasePostgres, config.DatabaseMysql)) + } + + // see: https://gorm.io/docs/gorm_config.html + cfg := gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + NoLowerCase: true, + }, + PrepareStmt: false, + } + + // Silence gorm query errors unless when not in debug mode + if config.GetLogLevel() != logger.DebugLevel { + cfg.Logger = gormlogger.Default.LogMode(gormlogger.Silent) + } + + return gorm.Open(d, &cfg) +} diff --git a/backend/internal/database/helpers.go b/backend/internal/database/helpers.go new file mode 100644 index 000000000..c34208bd7 --- /dev/null +++ b/backend/internal/database/helpers.go @@ -0,0 +1,40 @@ +package database + +import ( + "fmt" + "strings" + + "npm/internal/config" +) + +const ( + // DateFormat for DateFormat + DateFormat = "2006-01-02" + // DateTimeFormat for DateTimeFormat + DateTimeFormat = "2006-01-02T15:04:05" +) + +// QuoteTableName is a special function that will quote a table +// name based on the driver. Gorm normally handles this but this +// is for special cases where we run raw sql +func QuoteTableName(tbl string) string { + switch strings.ToLower(config.Configuration.DB.Driver) { + case config.DatabaseMysql: + // backticks for mysql + return fmt.Sprintf("`%s`", tbl) + default: + // double quotes for everything else + return fmt.Sprintf(`"%s"`, tbl) + } +} + +// GetCaseInsensitiveLike returns a different operator based on +// the db driver +func GetCaseInsensitiveLike() string { + switch strings.ToLower(config.Configuration.DB.Driver) { + case config.DatabasePostgres: + return "ILIKE" + default: + return "LIKE" + } +} diff --git a/backend/internal/database/migrator.go b/backend/internal/database/migrator.go new file mode 100644 index 000000000..5d5555be9 --- /dev/null +++ b/backend/internal/database/migrator.go @@ -0,0 +1,48 @@ +package database + +import ( + "fmt" + "net/url" + + "npm/embed" + "npm/internal/config" + "npm/internal/logger" + + "github.com/amacneil/dbmate/v2/pkg/dbmate" + + // Drivers: + _ "github.com/amacneil/dbmate/v2/pkg/driver/mysql" + _ "github.com/amacneil/dbmate/v2/pkg/driver/postgres" + _ "github.com/amacneil/dbmate/v2/pkg/driver/sqlite" +) + +type afterMigrationComplete func() + +// Migrate will bring the db up to date +func Migrate(followup afterMigrationComplete) bool { + dbURL := config.Configuration.DB.GetDBMateConnectURL() + u, _ := url.Parse(dbURL) + db := dbmate.New(u) + db.AutoDumpSchema = false + db.FS = embed.MigrationFiles + db.MigrationsDir = []string{fmt.Sprintf("./migrations/%s", config.Configuration.DB.GetDriver())} + + migrations, err := db.FindMigrations() + if err != nil { + logger.Error("MigrationError", err) + return false + } + + for _, m := range migrations { + logger.Debug("%s: %s", m.Version, m.FilePath) + } + + err = db.CreateAndMigrate() + if err != nil { + logger.Error("MigrationError", err) + return false + } + + followup() + return true +} diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js deleted file mode 100644 index d35fec257..000000000 --- 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/dnsproviders/common.go b/backend/internal/dnsproviders/common.go new file mode 100644 index 000000000..fef52e16e --- /dev/null +++ b/backend/internal/dnsproviders/common.go @@ -0,0 +1,116 @@ +package dnsproviders + +import ( + "encoding/json" + + "npm/internal/errors" +) + +// providerField should mimick jsonschema, so that +// the ui can render a field and validate it +// before we do. +// See: https://json-schema.org/draft/2020-12/json-schema-validation.html +type providerField struct { + Title string `json:"title"` + Type string `json:"type"` + AdditionalProperties bool `json:"additionalProperties"` + Minimum int `json:"minimum,omitempty"` + Maximum int `json:"maximum,omitempty"` + MinLength int `json:"minLength,omitempty"` + MaxLength int `json:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + IsSecret bool `json:"-"` // Not valid jsonschema +} + +// Provider is a simple struct +type Provider struct { + Title string `json:"title"` + Type string `json:"type"` // Should always be "object" + AdditionalProperties bool `json:"additionalProperties"` + MinProperties int `json:"minProperties,omitempty"` + Required []string `json:"required,omitempty"` + Properties map[string]providerField `json:"properties"` +} + +// GetJSONSchema encodes this object as JSON string +func (p *Provider) GetJSONSchema() (string, error) { + b, err := json.Marshal(p) + return string(b), err +} + +// ConvertToUpdatable will manipulate this object so that it returns +// an updatable json schema +func (p *Provider) ConvertToUpdatable() { + p.MinProperties = 1 + p.Required = nil +} + +// List returns an array of providers +func List() []Provider { + return []Provider{ + getDNSAcmeDNS(), + getDNSAd(), + getDNSAli(), + getDNSAws(), + getDNSAutoDNS(), + getDNSAzure(), + getDNSCf(), + getDNSCloudns(), + getDNSConoha(), + getDNSCx(), + getDNSCyon(), + getDNSDgon(), + getDNSMe(), + getDNSDNSimple(), + getDNSDa(), + getDNSDp(), + getDNSDpi(), + getDNSDreamhost(), + getDNSDuckDNS(), + getDNSDyn(), + getDNSDynu(), + getDNSEuserv(), + getDNSFreeDNS(), + getDNSGandiLiveDNS(), + getDNSGd(), + getDNSHe(), + getDNSInfoblox(), + getDNSInwx(), + getDNSIspconfig(), + getDNSKinghost(), + getDNSLinodeV4(), + getDNSLoopia(), + getDNSLua(), + getDNSNamecom(), + getDNSNamesilo(), + getDNSOne(), + getDNSYandex(), + getDNSSelectel(), + getDNSServercow(), + getDNSTele3(), + getDNSPDNS(), + getDNSUnoeuro(), + getDNSVscale(), + getDNSDNZilore(), + getDNSZonomi(), + } +} + +// GetAll returns all the configured providers +func GetAll() map[string]Provider { + mp := make(map[string]Provider) + items := List() + for _, item := range items { + mp[item.Title] = item + } + return mp +} + +// Get returns a single provider by name +func Get(provider string) (Provider, error) { + all := GetAll() + if item, found := all[provider]; found { + return item, nil + } + return Provider{}, errors.ErrProviderNotFound +} diff --git a/backend/internal/dnsproviders/common_test.go b/backend/internal/dnsproviders/common_test.go new file mode 100644 index 000000000..80bde44f1 --- /dev/null +++ b/backend/internal/dnsproviders/common_test.go @@ -0,0 +1,40 @@ +package dnsproviders + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetAll(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + providers := GetAll() + // This number will have to (annoyingly) be updated + // when adding new dns providers to the list + assert.Equal(t, 45, len(providers)) + + _, dynuExists := providers["dns_dynu"] + assert.Equal(t, true, dynuExists) + _, duckDNSExists := providers["dns_duckdns"] + assert.Equal(t, true, duckDNSExists) + _, cfExists := providers["dns_cf"] + assert.Equal(t, true, cfExists) + _, randomExists := providers["dns_shouldnotexist"] + assert.Equal(t, false, randomExists) +} + +func TestGet(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + provider, err := Get("dns_duckdns") + assert.Nil(t, err) + assert.Equal(t, "dns_duckdns", provider.Title) + + provider, err = Get("dns_shouldnotexist") + assert.NotNil(t, err) + assert.Equal(t, "provider_not_found", err.Error()) +} diff --git a/backend/internal/dnsproviders/dns_acmedns.go b/backend/internal/dnsproviders/dns_acmedns.go new file mode 100644 index 000000000..6730240dc --- /dev/null +++ b/backend/internal/dnsproviders/dns_acmedns.go @@ -0,0 +1,34 @@ +package dnsproviders + +func getDNSAcmeDNS() Provider { + return Provider{ + Title: "dns_acmedns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "ACMEDNS_BASE_URL", + "ACMEDNS_SUBDOMAIN", + "ACMEDNS_USERNAME", + "ACMEDNS_PASSWORD", + }, + Properties: map[string]providerField{ + "ACMEDNS_BASE_URL": { + Title: "base-url", + Type: "string", + }, + "ACMEDNS_SUBDOMAIN": { + Title: "subdomain", + Type: "string", + }, + "ACMEDNS_USERNAME": { + Title: "username", + Type: "string", + }, + "ACMEDNS_PASSWORD": { + Title: "password", + Type: "string", + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_acmedns_test.go b/backend/internal/dnsproviders/dns_acmedns_test.go new file mode 100644 index 000000000..6c5f25db4 --- /dev/null +++ b/backend/internal/dnsproviders/dns_acmedns_test.go @@ -0,0 +1,52 @@ +package dnsproviders + +import ( + "testing" + + "npm/internal/util" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestAcmeDNSProvider(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + provider := getDNSAcmeDNS() + json, err := provider.GetJSONSchema() + assert.Nil(t, err) + assert.Equal(t, `{ + "title": "dns_acmedns", + "type": "object", + "additionalProperties": false, + "required": [ + "ACMEDNS_BASE_URL", + "ACMEDNS_SUBDOMAIN", + "ACMEDNS_USERNAME", + "ACMEDNS_PASSWORD" + ], + "properties": { + "ACMEDNS_BASE_URL": { + "title": "base-url", + "type": "string", + "additionalProperties": false + }, + "ACMEDNS_PASSWORD": { + "title": "password", + "type": "string", + "additionalProperties": false + }, + "ACMEDNS_SUBDOMAIN": { + "title": "subdomain", + "type": "string", + "additionalProperties": false + }, + "ACMEDNS_USERNAME": { + "title": "username", + "type": "string", + "additionalProperties": false + } + } +}`, util.PrettyPrintJSON(json)) +} diff --git a/backend/internal/dnsproviders/dns_ad.go b/backend/internal/dnsproviders/dns_ad.go new file mode 100644 index 000000000..243f23ca7 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ad.go @@ -0,0 +1,19 @@ +package dnsproviders + +func getDNSAd() Provider { + return Provider{ + Title: "dns_ad", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "AD_API_KEY", + }, + Properties: map[string]providerField{ + "AD_API_KEY": { + Title: "api-key", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_ad_test.go b/backend/internal/dnsproviders/dns_ad_test.go new file mode 100644 index 000000000..4bf3b4716 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ad_test.go @@ -0,0 +1,34 @@ +package dnsproviders + +import ( + "testing" + + "npm/internal/util" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestAdProvider(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + provider := getDNSAd() + provider.ConvertToUpdatable() + json, err := provider.GetJSONSchema() + assert.Nil(t, err) + assert.Equal(t, `{ + "title": "dns_ad", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "AD_API_KEY": { + "title": "api-key", + "type": "string", + "additionalProperties": false, + "minLength": 1 + } + } +}`, util.PrettyPrintJSON(json)) +} diff --git a/backend/internal/dnsproviders/dns_ali.go b/backend/internal/dnsproviders/dns_ali.go new file mode 100644 index 000000000..e338f2768 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ali.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSAli() Provider { + return Provider{ + Title: "dns_ali", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Ali_Key", + "Ali_Secret", + }, + Properties: map[string]providerField{ + "Ali_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + }, + "Ali_Secret": { + Title: "secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_ali_test.go b/backend/internal/dnsproviders/dns_ali_test.go new file mode 100644 index 000000000..857837398 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ali_test.go @@ -0,0 +1,42 @@ +package dnsproviders + +import ( + "testing" + + "npm/internal/util" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestAliProvider(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + provider := getDNSAli() + json, err := provider.GetJSONSchema() + assert.Nil(t, err) + assert.Equal(t, `{ + "title": "dns_ali", + "type": "object", + "additionalProperties": false, + "required": [ + "Ali_Key", + "Ali_Secret" + ], + "properties": { + "Ali_Key": { + "title": "api-key", + "type": "string", + "additionalProperties": false, + "minLength": 1 + }, + "Ali_Secret": { + "title": "secret", + "type": "string", + "additionalProperties": false, + "minLength": 1 + } + } +}`, util.PrettyPrintJSON(json)) +} diff --git a/backend/internal/dnsproviders/dns_autodns.go b/backend/internal/dnsproviders/dns_autodns.go new file mode 100644 index 000000000..bb04f7c4e --- /dev/null +++ b/backend/internal/dnsproviders/dns_autodns.go @@ -0,0 +1,32 @@ +package dnsproviders + +func getDNSAutoDNS() Provider { + return Provider{ + Title: "dns_autodns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "AUTODNS_USER", + "AUTODNS_PASSWORD", + "AUTODNS_CONTEXT", + }, + Properties: map[string]providerField{ + "AUTODNS_USER": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "AUTODNS_PASSWORD": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "AUTODNS_CONTEXT": { + Title: "context", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_aws.go b/backend/internal/dnsproviders/dns_aws.go new file mode 100644 index 000000000..aed069452 --- /dev/null +++ b/backend/internal/dnsproviders/dns_aws.go @@ -0,0 +1,30 @@ +package dnsproviders + +func getDNSAws() Provider { + return Provider{ + Title: "dns_aws", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + }, + Properties: map[string]providerField{ + "AWS_ACCESS_KEY_ID": { + Title: "access-key-id", + Type: "string", + MinLength: 10, + }, + "AWS_SECRET_ACCESS_KEY": { + Title: "secret-access-key", + Type: "string", + MinLength: 10, + IsSecret: true, + }, + "AWS_DNS_SLOWRATE": { + Title: "slow-rate", + Type: "integer", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_azure.go b/backend/internal/dnsproviders/dns_azure.go new file mode 100644 index 000000000..a8c5a4377 --- /dev/null +++ b/backend/internal/dnsproviders/dns_azure.go @@ -0,0 +1,38 @@ +package dnsproviders + +func getDNSAzure() Provider { + return Provider{ + Title: "dns_azure", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "AZUREDNS_SUBSCRIPTIONID", + "AZUREDNS_TENANTID", + "AZUREDNS_APPID", + "AZUREDNS_CLIENTSECRET", + }, + Properties: map[string]providerField{ + "AZUREDNS_SUBSCRIPTIONID": { + Title: "subscription-id", + Type: "string", + MinLength: 1, + }, + "AZUREDNS_TENANTID": { + Title: "tenant-id", + Type: "string", + MinLength: 1, + }, + "AZUREDNS_APPID": { + Title: "app-id", + Type: "string", + MinLength: 1, + }, + "AZUREDNS_CLIENTSECRET": { + Title: "client-secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cf.go b/backend/internal/dnsproviders/dns_cf.go new file mode 100644 index 000000000..b35695c28 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cf.go @@ -0,0 +1,43 @@ +package dnsproviders + +func getDNSCf() Provider { + return Provider{ + Title: "dns_cf", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "CF_Key", + "CF_Email", + "CF_Token", + "CF_Account_ID", + }, + Properties: map[string]providerField{ + "CF_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + }, + "CF_Email": { + Title: "email", + Type: "string", + MinLength: 5, + }, + "CF_Token": { + Title: "token", + Type: "string", + MinLength: 5, + IsSecret: true, + }, + "CF_Account_ID": { + Title: "account-id", + Type: "string", + MinLength: 1, + }, + "CF_Zone_ID": { + Title: "zone-id", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cloudns.go b/backend/internal/dnsproviders/dns_cloudns.go new file mode 100644 index 000000000..297de58de --- /dev/null +++ b/backend/internal/dnsproviders/dns_cloudns.go @@ -0,0 +1,32 @@ +package dnsproviders + +func getDNSCloudns() Provider { + return Provider{ + Title: "dns_cloudns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "CLOUDNS_AUTH_ID", + "CLOUDNS_SUB_AUTH_ID", + "CLOUDNS_AUTH_PASSWORD", + }, + Properties: map[string]providerField{ + "CLOUDNS_AUTH_ID": { + Title: "auth-id", + Type: "string", + MinLength: 1, + }, + "CLOUDNS_SUB_AUTH_ID": { + Title: "sub-auth-id", + Type: "string", + MinLength: 1, + }, + "CLOUDNS_AUTH_PASSWORD": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_conoha.go b/backend/internal/dnsproviders/dns_conoha.go new file mode 100644 index 000000000..d84aacd41 --- /dev/null +++ b/backend/internal/dnsproviders/dns_conoha.go @@ -0,0 +1,38 @@ +package dnsproviders + +func getDNSConoha() Provider { + return Provider{ + Title: "dns_conoha", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "CONOHA_IdentityServiceApi", + "CONOHA_Username", + "CONOHA_Password", + "CONOHA_TenantId", + }, + Properties: map[string]providerField{ + "CONOHA_IdentityServiceApi": { + Title: "api-url", + Type: "string", + MinLength: 4, + }, + "CONOHA_Username": { + Title: "username", + Type: "string", + MinLength: 1, + }, + "CONOHA_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "CONOHA_TenantId": { + Title: "tenant-id", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cx.go b/backend/internal/dnsproviders/dns_cx.go new file mode 100644 index 000000000..2750124cf --- /dev/null +++ b/backend/internal/dnsproviders/dns_cx.go @@ -0,0 +1,24 @@ +package dnsproviders + +func getDNSCx() Provider { + return Provider{ + Title: "dns_cx", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "CX_Key", + "CX_Secret", + }, + Properties: map[string]providerField{ + "CX_Key": { + Title: "key", + Type: "string", + }, + "CX_Secret": { + Title: "secret", + Type: "string", + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_cyon.go b/backend/internal/dnsproviders/dns_cyon.go new file mode 100644 index 000000000..87a5ab379 --- /dev/null +++ b/backend/internal/dnsproviders/dns_cyon.go @@ -0,0 +1,33 @@ +package dnsproviders + +func getDNSCyon() Provider { + return Provider{ + Title: "dns_cyon", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "CY_Username", + "CY_Password", + "CY_OTP_Secret", + }, + Properties: map[string]providerField{ + "CY_Username": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "CY_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "CY_OTP_Secret": { + Title: "otp-secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_da.go b/backend/internal/dnsproviders/dns_da.go new file mode 100644 index 000000000..ee9e0f613 --- /dev/null +++ b/backend/internal/dnsproviders/dns_da.go @@ -0,0 +1,23 @@ +package dnsproviders + +func getDNSDa() Provider { + return Provider{ + Title: "dns_da", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DA_Api", + }, + Properties: map[string]providerField{ + "DA_Api": { + Title: "api-url", + Type: "string", + MinLength: 4, + }, + "DA_Api_Insecure": { + Title: "insecure", + Type: "boolean", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dgon.go b/backend/internal/dnsproviders/dns_dgon.go new file mode 100644 index 000000000..91113cceb --- /dev/null +++ b/backend/internal/dnsproviders/dns_dgon.go @@ -0,0 +1,19 @@ +package dnsproviders + +func getDNSDgon() Provider { + return Provider{ + Title: "dns_dgon", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DO_API_KEY", + }, + Properties: map[string]providerField{ + "DO_API_KEY": { + Title: "api-key", + Type: "string", + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dnsimple.go b/backend/internal/dnsproviders/dns_dnsimple.go new file mode 100644 index 000000000..a4a02ae16 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dnsimple.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSDNSimple() Provider { + return Provider{ + Title: "dns_dnsimple", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DNSimple_OAUTH_TOKEN", + }, + Properties: map[string]providerField{ + "DNSimple_OAUTH_TOKEN": { + Title: "oauth-token", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dp.go b/backend/internal/dnsproviders/dns_dp.go new file mode 100644 index 000000000..cece1a013 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dp.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSDp() Provider { + return Provider{ + Title: "dns_dp", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DP_Id", + "DP_Key", + }, + Properties: map[string]providerField{ + "DP_Id": { + Title: "id", + Type: "string", + MinLength: 1, + }, + "DP_Key": { + Title: "key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dpi.go b/backend/internal/dnsproviders/dns_dpi.go new file mode 100644 index 000000000..0b5255c58 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dpi.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSDpi() Provider { + return Provider{ + Title: "dns_dpi", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DPI_Id", + "DPI_Key", + }, + Properties: map[string]providerField{ + "DPI_Id": { + Title: "id", + Type: "string", + MinLength: 1, + }, + "DPI_Key": { + Title: "key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dreamhost.go b/backend/internal/dnsproviders/dns_dreamhost.go new file mode 100644 index 000000000..037c5f84e --- /dev/null +++ b/backend/internal/dnsproviders/dns_dreamhost.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSDreamhost() Provider { + return Provider{ + Title: "dns_dreamhost", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DH_API_KEY", + }, + Properties: map[string]providerField{ + "DH_API_KEY": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_duckdns.go b/backend/internal/dnsproviders/dns_duckdns.go new file mode 100644 index 000000000..3407ca0cb --- /dev/null +++ b/backend/internal/dnsproviders/dns_duckdns.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSDuckDNS() Provider { + return Provider{ + Title: "dns_duckdns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DuckDNS_Token", + }, + Properties: map[string]providerField{ + "DuckDNS_Token": { + Title: "token", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dyn.go b/backend/internal/dnsproviders/dns_dyn.go new file mode 100644 index 000000000..be595ce31 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dyn.go @@ -0,0 +1,32 @@ +package dnsproviders + +func getDNSDyn() Provider { + return Provider{ + Title: "dns_dyn", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "DYN_Customer", + "DYN_Username", + "DYN_Password", + }, + Properties: map[string]providerField{ + "DYN_Customer": { + Title: "customer", + Type: "string", + MinLength: 1, + }, + "DYN_Username": { + Title: "username", + Type: "string", + MinLength: 1, + }, + "DYN_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_dynu.go b/backend/internal/dnsproviders/dns_dynu.go new file mode 100644 index 000000000..1f3024c19 --- /dev/null +++ b/backend/internal/dnsproviders/dns_dynu.go @@ -0,0 +1,25 @@ +package dnsproviders + +func getDNSDynu() Provider { + return Provider{ + Title: "dns_dynu", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Dynu_ClientId", + }, + Properties: map[string]providerField{ + "Dynu_ClientId": { + Title: "client-id", + Type: "string", + MinLength: 1, + }, + "Dynu_Secret": { + Title: "secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_euserv.go b/backend/internal/dnsproviders/dns_euserv.go new file mode 100644 index 000000000..be500480c --- /dev/null +++ b/backend/internal/dnsproviders/dns_euserv.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSEuserv() Provider { + return Provider{ + Title: "dns_euserv", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "EUSERV_Username", + "EUSERV_Password", + }, + Properties: map[string]providerField{ + "EUSERV_Username": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "EUSERV_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_freedns.go b/backend/internal/dnsproviders/dns_freedns.go new file mode 100644 index 000000000..7c6849b10 --- /dev/null +++ b/backend/internal/dnsproviders/dns_freedns.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSFreeDNS() Provider { + return Provider{ + Title: "dns_freedns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "FREEDNS_User", + "FREEDNS_Password", + }, + Properties: map[string]providerField{ + "FREEDNS_User": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "FREEDNS_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_gandi_livedns.go b/backend/internal/dnsproviders/dns_gandi_livedns.go new file mode 100644 index 000000000..a22ebbff6 --- /dev/null +++ b/backend/internal/dnsproviders/dns_gandi_livedns.go @@ -0,0 +1,19 @@ +package dnsproviders + +func getDNSGandiLiveDNS() Provider { + return Provider{ + Title: "dns_gandi_livedns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "GANDI_LIVEDNS_KEY", + }, + Properties: map[string]providerField{ + "GANDI_LIVEDNS_KEY": { + Title: "key", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_gd.go b/backend/internal/dnsproviders/dns_gd.go new file mode 100644 index 000000000..11248948f --- /dev/null +++ b/backend/internal/dnsproviders/dns_gd.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSGd() Provider { + return Provider{ + Title: "dns_gd", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "GD_Key", + "GD_Secret", + }, + Properties: map[string]providerField{ + "GD_Key": { + Title: "key", + Type: "string", + MinLength: 1, + }, + "GD_Secret": { + Title: "secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_he.go b/backend/internal/dnsproviders/dns_he.go new file mode 100644 index 000000000..f4aed32bc --- /dev/null +++ b/backend/internal/dnsproviders/dns_he.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSHe() Provider { + return Provider{ + Title: "dns_he", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "HE_Username", + "HE_Password", + }, + Properties: map[string]providerField{ + "HE_Username": { + Title: "username", + Type: "string", + MinLength: 1, + }, + "HE_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_infoblox.go b/backend/internal/dnsproviders/dns_infoblox.go new file mode 100644 index 000000000..f356f36c5 --- /dev/null +++ b/backend/internal/dnsproviders/dns_infoblox.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSInfoblox() Provider { + return Provider{ + Title: "dns_infoblox", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Infoblox_Creds", + "Infoblox_Server", + }, + Properties: map[string]providerField{ + "Infoblox_Creds": { + Title: "credentials", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "Infoblox_Server": { + Title: "server", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_inwx.go b/backend/internal/dnsproviders/dns_inwx.go new file mode 100644 index 000000000..c0f75cca2 --- /dev/null +++ b/backend/internal/dnsproviders/dns_inwx.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSInwx() Provider { + return Provider{ + Title: "dns_inwx", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "INWX_User", + "INWX_Password", + }, + Properties: map[string]providerField{ + "INWX_User": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "INWX_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_ispconfig.go b/backend/internal/dnsproviders/dns_ispconfig.go new file mode 100644 index 000000000..f10e0f799 --- /dev/null +++ b/backend/internal/dnsproviders/dns_ispconfig.go @@ -0,0 +1,36 @@ +package dnsproviders + +func getDNSIspconfig() Provider { + return Provider{ + Title: "dns_ispconfig", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "ISPC_User", + "ISPC_Password", + "ISPC_Api", + }, + Properties: map[string]providerField{ + "ISPC_User": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "ISPC_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "ISPC_Api": { + Title: "api-url", + Type: "string", + MinLength: 1, + }, + "ISPC_Api_Insecure": { + Title: "insecure", + Type: "boolean", + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_kinghost.go b/backend/internal/dnsproviders/dns_kinghost.go new file mode 100644 index 000000000..a497acc8c --- /dev/null +++ b/backend/internal/dnsproviders/dns_kinghost.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSKinghost() Provider { + return Provider{ + Title: "dns_kinghost", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "KINGHOST_Username", + "KINGHOST_Password", + }, + Properties: map[string]providerField{ + "KINGHOST_Username": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "KINGHOST_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_linode_v4.go b/backend/internal/dnsproviders/dns_linode_v4.go new file mode 100644 index 000000000..ddafeca17 --- /dev/null +++ b/backend/internal/dnsproviders/dns_linode_v4.go @@ -0,0 +1,22 @@ +package dnsproviders + +// Note: https://github.com/acmesh-official/acme.sh/wiki/dnsapi#14-use-linode-domain-api +// needs 15 minute sleep, not currently implemented +func getDNSLinodeV4() Provider { + return Provider{ + Title: "dns_linode_v4", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "LINODE_V4_API_KEY", + }, + Properties: map[string]providerField{ + "LINODE_V4_API_KEY": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_loopia.go b/backend/internal/dnsproviders/dns_loopia.go new file mode 100644 index 000000000..64d22d329 --- /dev/null +++ b/backend/internal/dnsproviders/dns_loopia.go @@ -0,0 +1,32 @@ +package dnsproviders + +func getDNSLoopia() Provider { + return Provider{ + Title: "dns_loopia", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "LOOPIA_Api", + "LOOPIA_User", + "LOOPIA_Password", + }, + Properties: map[string]providerField{ + "LOOPIA_Api": { + Title: "api-url", + Type: "string", + MinLength: 4, + }, + "LOOPIA_User": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "LOOPIA_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_lua.go b/backend/internal/dnsproviders/dns_lua.go new file mode 100644 index 000000000..b79a87a7b --- /dev/null +++ b/backend/internal/dnsproviders/dns_lua.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSLua() Provider { + return Provider{ + Title: "dns_lua", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "LUA_Key", + "LUA_Email", + }, + Properties: map[string]providerField{ + "LUA_Key": { + Title: "key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "LUA_Email": { + Title: "email", + Type: "string", + MinLength: 5, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_me.go b/backend/internal/dnsproviders/dns_me.go new file mode 100644 index 000000000..96b3d4a2a --- /dev/null +++ b/backend/internal/dnsproviders/dns_me.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSMe() Provider { + return Provider{ + Title: "dns_me", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "ME_Key", + "ME_Secret", + }, + Properties: map[string]providerField{ + "ME_Key": { + Title: "key", + Type: "string", + MinLength: 1, + }, + "ME_Secret": { + Title: "secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_namecom.go b/backend/internal/dnsproviders/dns_namecom.go new file mode 100644 index 000000000..9a72de6cb --- /dev/null +++ b/backend/internal/dnsproviders/dns_namecom.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSNamecom() Provider { + return Provider{ + Title: "dns_namecom", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Namecom_Username", + "Namecom_Token", + }, + Properties: map[string]providerField{ + "Namecom_Username": { + Title: "username", + Type: "string", + MinLength: 1, + }, + "Namecom_Token": { + Title: "token", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_namesilo.go b/backend/internal/dnsproviders/dns_namesilo.go new file mode 100644 index 000000000..d44482131 --- /dev/null +++ b/backend/internal/dnsproviders/dns_namesilo.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSNamesilo() Provider { + return Provider{ + Title: "dns_namesilo", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Namesilo_Key", + }, + Properties: map[string]providerField{ + "Namesilo_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_nsone.go b/backend/internal/dnsproviders/dns_nsone.go new file mode 100644 index 000000000..36c4594ff --- /dev/null +++ b/backend/internal/dnsproviders/dns_nsone.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSOne() Provider { + return Provider{ + Title: "dns_nsone", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "NS1_Key", + }, + Properties: map[string]providerField{ + "NS1_Key": { + Title: "key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_pdns.go b/backend/internal/dnsproviders/dns_pdns.go new file mode 100644 index 000000000..ee3802839 --- /dev/null +++ b/backend/internal/dnsproviders/dns_pdns.go @@ -0,0 +1,37 @@ +package dnsproviders + +func getDNSPDNS() Provider { + return Provider{ + Title: "dns_pdns", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "PDNS_Url", + "PDNS_ServerId", + "PDNS_Token", + "PDNS_Ttl", + }, + Properties: map[string]providerField{ + "PDNS_Url": { + Title: "url", + Type: "string", + MinLength: 1, + }, + "PDNS_ServerId": { + Title: "server-id", + Type: "string", + MinLength: 1, + }, + "PDNS_Token": { + Title: "token", + Type: "string", + MinLength: 1, + }, + "PDNS_Ttl": { + Title: "ttl", + Type: "integer", + Minimum: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_selectel.go b/backend/internal/dnsproviders/dns_selectel.go new file mode 100644 index 000000000..7e97dce12 --- /dev/null +++ b/backend/internal/dnsproviders/dns_selectel.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSSelectel() Provider { + return Provider{ + Title: "dns_selectel", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "SL_Key", + }, + Properties: map[string]providerField{ + "SL_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_servercow.go b/backend/internal/dnsproviders/dns_servercow.go new file mode 100644 index 000000000..007d49d13 --- /dev/null +++ b/backend/internal/dnsproviders/dns_servercow.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSServercow() Provider { + return Provider{ + Title: "dns_servercow", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "SERVERCOW_API_Username", + "SERVERCOW_API_Password", + }, + Properties: map[string]providerField{ + "SERVERCOW_API_Username": { + Title: "user", + Type: "string", + MinLength: 1, + }, + "SERVERCOW_API_Password": { + Title: "password", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_tele3.go b/backend/internal/dnsproviders/dns_tele3.go new file mode 100644 index 000000000..c40b8c7be --- /dev/null +++ b/backend/internal/dnsproviders/dns_tele3.go @@ -0,0 +1,26 @@ +package dnsproviders + +func getDNSTele3() Provider { + return Provider{ + Title: "dns_tele3", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "TELE3_Key", + "TELE3_Secret", + }, + Properties: map[string]providerField{ + "TELE3_Key": { + Title: "key", + Type: "string", + MinLength: 1, + }, + "TELE3_Secret": { + Title: "secret", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_unoeuro.go b/backend/internal/dnsproviders/dns_unoeuro.go new file mode 100644 index 000000000..63b6fa3fc --- /dev/null +++ b/backend/internal/dnsproviders/dns_unoeuro.go @@ -0,0 +1,27 @@ +package dnsproviders + +func getDNSUnoeuro() Provider { + return Provider{ + Title: "dns_unoeuro", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "UNO_Key", + "UNO_User", + }, + Properties: map[string]providerField{ + "UNO_Key": { + Title: "key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + "UNO_User": { + Title: "user", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_vscale.go b/backend/internal/dnsproviders/dns_vscale.go new file mode 100644 index 000000000..86f1cbf5d --- /dev/null +++ b/backend/internal/dnsproviders/dns_vscale.go @@ -0,0 +1,19 @@ +package dnsproviders + +func getDNSVscale() Provider { + return Provider{ + Title: "dns_vscale", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "VSCALE_API_KEY", + }, + Properties: map[string]providerField{ + "VSCALE_API_KEY": { + Title: "api-key", + Type: "string", + MinLength: 1, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_yandex.go b/backend/internal/dnsproviders/dns_yandex.go new file mode 100644 index 000000000..88ab7b470 --- /dev/null +++ b/backend/internal/dnsproviders/dns_yandex.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSYandex() Provider { + return Provider{ + Title: "dns_yandex", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "PDD_Token", + }, + Properties: map[string]providerField{ + "PDD_Token": { + Title: "token", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_zilore.go b/backend/internal/dnsproviders/dns_zilore.go new file mode 100644 index 000000000..b35f21591 --- /dev/null +++ b/backend/internal/dnsproviders/dns_zilore.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSDNZilore() Provider { + return Provider{ + Title: "dns_zilore", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "Zilore_Key", + }, + Properties: map[string]providerField{ + "Zilore_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/dnsproviders/dns_zonomi.go b/backend/internal/dnsproviders/dns_zonomi.go new file mode 100644 index 000000000..f93f3ef84 --- /dev/null +++ b/backend/internal/dnsproviders/dns_zonomi.go @@ -0,0 +1,20 @@ +package dnsproviders + +func getDNSZonomi() Provider { + return Provider{ + Title: "dns_zonomi", + Type: "object", + AdditionalProperties: false, + Required: []string{ + "ZM_Key", + }, + Properties: map[string]providerField{ + "ZM_Key": { + Title: "api-key", + Type: "string", + MinLength: 1, + IsSecret: true, + }, + }, + } +} diff --git a/backend/internal/entity/accesslist/methods.go b/backend/internal/entity/accesslist/methods.go new file mode 100644 index 000000000..b0c9ad892 --- /dev/null +++ b/backend/internal/entity/accesslist/methods.go @@ -0,0 +1,50 @@ +package accesslist + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of access lists +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/accesslist/model.go b/backend/internal/entity/accesslist/model.go new file mode 100644 index 000000000..64e0a0587 --- /dev/null +++ b/backend/internal/entity/accesslist/model.go @@ -0,0 +1,54 @@ +package accesslist + +import ( + "npm/internal/database" + "npm/internal/entity/user" + "npm/internal/model" + "npm/internal/types" + + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + Meta types.JSONB `json:"meta" gorm:"column:meta"` + // Expansions + User *user.Model `json:"user,omitempty" gorm:"-"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "access_list" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil +} diff --git a/backend/internal/entity/auth/entity_test.go b/backend/internal/entity/auth/entity_test.go new file mode 100644 index 000000000..b4b6f8d42 --- /dev/null +++ b/backend/internal/entity/auth/entity_test.go @@ -0,0 +1,166 @@ +package auth + +import ( + "regexp" + "testing" + + "npm/internal/test" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + singleRow *sqlmock.Rows +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.singleRow = sqlmock.NewRows([]string{ + "id", + "user_id", + "type", + "secret", + }).AddRow( + 10, + 100, + TypeLocal, + "abc123", + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +func assertModel(t *testing.T, m Model) { + assert.Equal(t, uint(10), m.ID) + assert.Equal(t, uint(100), m.UserID) + assert.Equal(t, TypeLocal, m.Type) + assert.Equal(t, "abc123", m.Secret) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE "auth"."id" = $1 AND "auth"."is_deleted" = $2 ORDER BY "auth"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestGetByUserIDType() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)). + WithArgs(100, TypeLocal, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByUserIDType(100, TypeLocal) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestGetByIdenityType() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE identity = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)). + WithArgs("johndoe", TypeLocal, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByIdenityType("johndoe", TypeLocal) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestSave() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "auth" ("created_at","updated_at","is_deleted","user_id","type","identity","secret") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + 100, + TypeLocal, + "", + "abc123", + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + // New model + m := Model{ + UserID: 100, + Type: TypeLocal, + Secret: "abc123", + } + err := m.Save() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSetPassword() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + m := Model{UserID: 100} + err := m.SetPassword("abc123") + require.NoError(s.T(), err) + assert.Equal(s.T(), TypeLocal, m.Type) + assert.Greater(s.T(), len(m.Secret), 15) +} + +func (s *testsuite) TestValidateSecret() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + m := Model{UserID: 100} + m.SetPassword("abc123") + + err := m.ValidateSecret("abc123") + require.NoError(s.T(), err) + err = m.ValidateSecret("this is not the password") + assert.NotNil(s.T(), err) + assert.Equal(s.T(), "Invalid Credentials", err.Error()) + + m.Type = "not a valid type" + err = m.ValidateSecret("abc123") + assert.NotNil(s.T(), err) + assert.Equal(s.T(), "Could not validate Secret, auth type is not Local", err.Error()) +} diff --git a/backend/internal/entity/auth/ldap.go b/backend/internal/entity/auth/ldap.go new file mode 100644 index 000000000..9a72f0cff --- /dev/null +++ b/backend/internal/entity/auth/ldap.go @@ -0,0 +1,96 @@ +package auth + +import ( + "encoding/json" + "fmt" + "strings" + + "npm/internal/entity/setting" + "npm/internal/logger" + + ldap3 "github.com/go-ldap/ldap/v3" + "github.com/rotisserie/eris" +) + +// LDAPUser is the LDAP User +type LDAPUser struct { + Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` +} + +// LDAPAuthenticate will use ldap to authenticate with user/pass +func LDAPAuthenticate(identity, password string) (*LDAPUser, error) { + ldapSettings, err := setting.GetLDAPSettings() + if err != nil { + return nil, err + } + + dn := strings.Replace(ldapSettings.UserDN, "{{USERNAME}}", identity, 1) + conn, err := ldapConnect(ldapSettings.Host, dn, password) + if err != nil { + return nil, err + } + // nolint: errcheck, gosec + defer conn.Close() + return ldapSearchUser(conn, ldapSettings, identity) +} + +// Attempt ldap connection +func ldapConnect(host, dn, password string) (*ldap3.Conn, error) { + var conn *ldap3.Conn + var err error + + if conn, err = ldap3.DialURL(fmt.Sprintf("ldap://%s", host)); err != nil { + logger.Error("LdapError", err) + return nil, err + } + + logger.Debug("LDAP Logging in with: %s", dn) + if err := conn.Bind(dn, password); err != nil { + if !strings.Contains(err.Error(), "Invalid Credentials") { + logger.Error("LDAPAuthError", err) + } + // nolint: gosec, errcheck + conn.Close() + return nil, err + } + + logger.Debug("LDAP Login Successful") + return conn, nil +} + +func ldapSearchUser(l *ldap3.Conn, ldapSettings setting.LDAPSettings, username string) (*LDAPUser, error) { + // Search for the given username + searchRequest := ldap3.NewSearchRequest( + ldapSettings.BaseDN, + ldap3.ScopeWholeSubtree, + ldap3.NeverDerefAliases, + 0, + 0, + false, + strings.Replace(ldapSettings.SelfFilter, "{{USERNAME}}", username, 1), + nil, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + logger.Error("LdapError", err) + return nil, err + } + + if len(sr.Entries) < 1 { + return nil, eris.New("No user found in LDAP search") + } else if len(sr.Entries) > 1 { + j, _ := json.Marshal(sr) + logger.Debug("LDAP Search Results: %s", j) + return nil, eris.Errorf("Too many LDAP results returned in LDAP search: %d", len(sr.Entries)) + } + + return &LDAPUser{ + Username: strings.ToLower(username), + Name: sr.Entries[0].GetAttributeValue(ldapSettings.NameProperty), + Email: strings.ToLower(sr.Entries[0].GetAttributeValue(ldapSettings.EmailProperty)), + }, nil +} diff --git a/backend/internal/entity/auth/methods.go b/backend/internal/entity/auth/methods.go new file mode 100644 index 000000000..8cce3f332 --- /dev/null +++ b/backend/internal/entity/auth/methods.go @@ -0,0 +1,34 @@ +package auth + +import ( + "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 id and type +func GetByUserIDType(userID uint, authType string) (Model, error) { + var auth Model + db := database.GetDB() + result := db. + Where("user_id = ?", userID). + Where("type = ?", authType). + First(&auth) + return auth, result.Error +} + +// GetByIdenityType finds a user by identity and type +func GetByIdenityType(identity string, authType string) (Model, error) { + var auth Model + db := database.GetDB() + result := db. + Where("identity = ?", identity). + Where("type = ?", authType). + First(&auth) + return auth, result.Error +} diff --git a/backend/internal/entity/auth/model.go b/backend/internal/entity/auth/model.go new file mode 100644 index 000000000..38a556278 --- /dev/null +++ b/backend/internal/entity/auth/model.go @@ -0,0 +1,71 @@ +package auth + +import ( + "npm/internal/database" + "npm/internal/model" + + "github.com/rotisserie/eris" + "golang.org/x/crypto/bcrypt" +) + +// Auth types +const ( + TypeLocal = "local" + TypeLDAP = "ldap" + TypeOAuth = "oauth" +) + +// Model is the model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id"` + Type string `json:"type" gorm:"column:type;default:local"` + Identity string `json:"identity,omitempty" gorm:"column:identity"` + Secret string `json:"secret,omitempty" gorm:"column:secret"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "auth" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// 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 = TypeLocal + 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 != TypeLocal { + return eris.New("Could not validate Secret, auth type is not Local") + } + + err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret)) + if err != nil { + return eris.New("Invalid Credentials") + } + + return nil +} diff --git a/backend/internal/entity/auth/oauth.go b/backend/internal/entity/auth/oauth.go new file mode 100644 index 000000000..438eb87ea --- /dev/null +++ b/backend/internal/entity/auth/oauth.go @@ -0,0 +1,218 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "npm/internal/entity/setting" + "npm/internal/logger" + + cache "github.com/patrickmn/go-cache" + "github.com/rotisserie/eris" + "golang.org/x/oauth2" +) + +// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints +var ( + OAuthCache *cache.Cache + settingGetOAuthSettings = setting.GetOAuthSettings +) + +// OAuthCacheInit will create a new Memory Cache +func OAuthCacheInit() { + if OAuthCache == nil { + logger.Debug("Creating a new OAuthCache") + OAuthCache = cache.New(5*time.Minute, 5*time.Minute) + } +} + +// OAuthUser is the OAuth User +type OAuthUser struct { + Identifier string `json:"identifier"` + Token string `json:"token"` + Resource map[string]any `json:"resource"` +} + +// GetResourceField will attempt to get a field from the resource +func (m *OAuthUser) GetResourceField(field string) string { + if m.Resource != nil { + if value, ok := m.Resource[field]; ok { + return value.(string) + } + } + return "" +} + +// GetID attempts to get an ID from the resource +func (m *OAuthUser) GetID() string { + if m.Identifier != "" { + return m.Identifier + } + + fields := []string{ + "uid", + "user_id", + "username", + "preferred_username", + "email", + "mail", + } + + for _, field := range fields { + if val := m.GetResourceField(field); val != "" { + return val + } + } + + return "" +} + +// GetName attempts to get a name from the resource +// using different fields +func (m *OAuthUser) GetName() string { + fields := []string{ + "nickname", + "given_name", + "name", + "preferred_username", + "username", + } + + for _, field := range fields { + if name := m.GetResourceField(field); name != "" { + return name + } + } + + // Fallback: + return m.Identifier +} + +// GetEmail will return an email address even if it can't be known in the +// Resource +func (m *OAuthUser) GetEmail() string { + // See if there's an email field first + if email := m.GetResourceField("email"); email != "" { + return email + } + + // Return the identifier if it looks like an email + if m.Identifier != "" { + if strings.Contains(m.Identifier, "@") { + return m.Identifier + } + return fmt.Sprintf("%s@oauth", m.Identifier) + } + return "" +} + +func getOAuth2Config() (*oauth2.Config, *setting.OAuthSettings, error) { + oauthSettings, err := settingGetOAuthSettings() + if err != nil { + return nil, nil, err + } + + if oauthSettings.ClientID == "" || oauthSettings.ClientSecret == "" || oauthSettings.AuthURL == "" || oauthSettings.TokenURL == "" { + return nil, nil, eris.New("oauth-settings-incorrect") + } + + return &oauth2.Config{ + ClientID: oauthSettings.ClientID, + ClientSecret: oauthSettings.ClientSecret, + Scopes: oauthSettings.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: oauthSettings.AuthURL, + TokenURL: oauthSettings.TokenURL, + }, + }, &oauthSettings, nil +} + +// OAuthLogin is hit by the client to generate a URL to redirect to +// and start the oauth process +func OAuthLogin(redirectBase, ipAddress string) (string, error) { + OAuthCacheInit() + + conf, _, err := getOAuth2Config() + if err != nil { + return "", err + } + + verifier := oauth2.GenerateVerifier() + OAuthCache.Set(getCacheKey(ipAddress), verifier, cache.DefaultExpiration) + + // todo: state should be unique to the incoming IP address of the requester, I guess + url := conf.AuthCodeURL("state", oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier)) + + if redirectBase != "" { + url = url + "&redirect_uri=" + redirectBase + "/oauth/redirect" + } + + logger.Debug("URL: %s", url) + return url, nil +} + +// OAuthReturn ... +func OAuthReturn(ctx context.Context, code, ipAddress string) (*OAuthUser, error) { + // Just in case... + OAuthCacheInit() + + conf, oauthSettings, err := getOAuth2Config() + if err != nil { + return nil, err + } + + verifier, found := OAuthCache.Get(getCacheKey(ipAddress)) + if !found { + return nil, eris.New("oauth-verifier-not-found") + } + + // Use the authorization code that is pushed to the redirect + // URL. Exchange will do the handshake to retrieve the + // initial access token. The HTTP Client returned by + // conf.Client will refresh the token as necessary. + tok, err := conf.Exchange(ctx, code, oauth2.VerifierOption(verifier.(string))) + if err != nil { + return nil, err + } + + // At this stage, the token is the JWT as given by the oauth server. + // we need to use that to get more info about this user, + // and then we'll create our own jwt for use later. + + client := conf.Client(ctx, tok) + resp, err := client.Get(oauthSettings.ResourceURL) + if err != nil { + return nil, err + } + + // nolint: errcheck, gosec + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + ou := OAuthUser{ + Token: tok.AccessToken, + } + + // unmarshal the body into a interface + if err := json.Unmarshal(body, &ou.Resource); err != nil { + return nil, err + } + + // Attempt to get the identifier from the resource + if oauthSettings.Identifier != "" { + ou.Identifier = ou.GetResourceField(oauthSettings.Identifier) + } + + return &ou, nil +} + +func getCacheKey(ipAddress string) string { + return fmt.Sprintf("oauth-%s", ipAddress) +} diff --git a/backend/internal/entity/auth/oauth_test.go b/backend/internal/entity/auth/oauth_test.go new file mode 100644 index 000000000..46efaea8b --- /dev/null +++ b/backend/internal/entity/auth/oauth_test.go @@ -0,0 +1,430 @@ +package auth + +import ( + "context" + "testing" + + "npm/internal/entity/setting" + + cache "github.com/patrickmn/go-cache" + "github.com/rotisserie/eris" + "github.com/stretchr/testify/assert" +) + +func TestGetOAuth2Config(t *testing.T) { + tests := []struct { + name string + mockSettings setting.OAuthSettings + expectedError error + }{ + { + name: "Valid settings", + mockSettings: setting.OAuthSettings{ + ClientID: "valid-client-id", + ClientSecret: "valid-client-secret", + AuthURL: "https://auth.url", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + }, + expectedError: nil, + }, + { + name: "Missing ClientID", + mockSettings: setting.OAuthSettings{ + ClientSecret: "valid-client-secret", + AuthURL: "https://auth.url", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + }, + expectedError: eris.New("oauth-settings-incorrect"), + }, + { + name: "Missing ClientSecret", + mockSettings: setting.OAuthSettings{ + ClientID: "valid-client-id", + AuthURL: "https://auth.url", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + }, + expectedError: eris.New("oauth-settings-incorrect"), + }, + { + name: "Missing AuthURL", + mockSettings: setting.OAuthSettings{ + ClientID: "valid-client-id", + ClientSecret: "valid-client-secret", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + }, + expectedError: eris.New("oauth-settings-incorrect"), + }, + { + name: "Missing TokenURL", + mockSettings: setting.OAuthSettings{ + ClientID: "valid-client-id", + ClientSecret: "valid-client-secret", + AuthURL: "https://auth.url", + Scopes: []string{"scope1", "scope2"}, + }, + expectedError: eris.New("oauth-settings-incorrect"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock the GetOAuthSettings function + settingGetOAuthSettings = func() (setting.OAuthSettings, error) { + return tt.mockSettings, nil + } + + config, settings, err := getOAuth2Config() + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + assert.NotNil(t, config) + assert.NotNil(t, settings) + assert.Equal(t, tt.mockSettings.ClientID, config.ClientID) + assert.Equal(t, tt.mockSettings.ClientSecret, config.ClientSecret) + assert.Equal(t, tt.mockSettings.AuthURL, config.Endpoint.AuthURL) + assert.Equal(t, tt.mockSettings.TokenURL, config.Endpoint.TokenURL) + assert.Equal(t, tt.mockSettings.Scopes, config.Scopes) + } + }) + } +} + +func TestGetEmail(t *testing.T) { + tests := []struct { + name string + oauthUser OAuthUser + expected string + }{ + { + name: "Email in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "email": "user@example.com", + }, + }, + expected: "user@example.com", + }, + { + name: "Identifier is email", + oauthUser: OAuthUser{ + Identifier: "user@example.com", + }, + expected: "user@example.com", + }, + { + name: "Identifier is not email", + oauthUser: OAuthUser{ + Identifier: "user123", + }, + expected: "user123@oauth", + }, + { + name: "No email or identifier", + oauthUser: OAuthUser{ + Resource: map[string]any{}, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + email := tt.oauthUser.GetEmail() + assert.Equal(t, tt.expected, email) + }) + } +} + +func TestGetName(t *testing.T) { + tests := []struct { + name string + oauthUser OAuthUser + expected string + }{ + { + name: "Nickname in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "nickname": "user_nick", + }, + }, + expected: "user_nick", + }, + { + name: "Given name in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "given_name": "User Given", + }, + }, + expected: "User Given", + }, + { + name: "Name in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "name": "User Name", + }, + }, + expected: "User Name", + }, + { + name: "Preferred username in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "preferred_username": "preferred_user", + }, + }, + expected: "preferred_user", + }, + { + name: "Username in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "username": "user123", + }, + }, + expected: "user123", + }, + { + name: "No name fields in resource, fallback to identifier", + oauthUser: OAuthUser{ + Identifier: "fallback_identifier", + Resource: map[string]any{}, + }, + expected: "fallback_identifier", + }, + { + name: "No name fields and no identifier", + oauthUser: OAuthUser{ + Resource: map[string]any{}, + }, + expected: "", + }, + { + name: "All fields", + oauthUser: OAuthUser{ + Identifier: "fallback_identifier", + Resource: map[string]any{ + "nickname": "user_nick", + "given_name": "User Given", + "name": "User Name", + "preferred_username": "preferred_user", + "username": "user123", + }, + }, + expected: "user_nick", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name := tt.oauthUser.GetName() + assert.Equal(t, tt.expected, name) + }) + } +} + +func TestGetID(t *testing.T) { + tests := []struct { + name string + oauthUser OAuthUser + expected string + }{ + { + name: "Identifier is set", + oauthUser: OAuthUser{ + Identifier: "user123", + }, + expected: "user123", + }, + { + name: "UID in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "uid": "uid123", + }, + }, + expected: "uid123", + }, + { + name: "User ID in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "user_id": "user_id123", + }, + }, + expected: "user_id123", + }, + { + name: "Username in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "username": "username123", + }, + }, + expected: "username123", + }, + { + name: "Preferred username in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "preferred_username": "preferred_user", + }, + }, + expected: "preferred_user", + }, + { + name: "Email in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "email": "user@example.com", + }, + }, + expected: "user@example.com", + }, + { + name: "Mail in resource", + oauthUser: OAuthUser{ + Resource: map[string]any{ + "mail": "mail@example.com", + }, + }, + expected: "mail@example.com", + }, + { + name: "No identifier or resource fields", + oauthUser: OAuthUser{ + Resource: map[string]any{}, + }, + expected: "", + }, + { + name: "All fields", + oauthUser: OAuthUser{ + Identifier: "user123", + Resource: map[string]any{ + "uid": "uid123", + "user_id": "user_id123", + "username": "username123", + "preferred_username": "preferred_user", + "mail": "mail@example.com", + "email": "email@example.com", + }, + }, + expected: "user123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := tt.oauthUser.GetID() + assert.Equal(t, tt.expected, id) + }) + } +} + +func TestOAuthLogin(t *testing.T) { + tests := []struct { + name string + redirectBase string + ipAddress string + expectedError error + }{ + { + name: "Valid redirect base", + redirectBase: "https://redirect.base", + ipAddress: "127.0.0.1", + expectedError: nil, + }, + { + name: "Empty redirect base", + redirectBase: "", + ipAddress: "127.0.0.1", + expectedError: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock the GetOAuthSettings function + settingGetOAuthSettings = func() (setting.OAuthSettings, error) { + return setting.OAuthSettings{ + ClientID: "valid-client-id", + ClientSecret: "valid-client-secret", + AuthURL: "https://auth.url", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + }, nil + } + + url, err := OAuthLogin(tt.redirectBase, tt.ipAddress) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, url) + } + }) + } +} + +func TestOAuthReturn(t *testing.T) { + var errNotFound = eris.New("oauth-verifier-not-found") + tests := []struct { + name string + code string + ipAddress string + expectedError error + }{ + { + name: "Invalid code", + code: "invalid-code", + ipAddress: "127.0.0.100", + expectedError: errNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock the GetOAuthSettings function + settingGetOAuthSettings = func() (setting.OAuthSettings, error) { + return setting.OAuthSettings{ + ClientID: "valid-client-id", + ClientSecret: "valid-client-secret", + AuthURL: "https://auth.url", + TokenURL: "https://token.url", + Scopes: []string{"scope1", "scope2"}, + ResourceURL: "https://resource.url", + Identifier: "id", + }, nil + } + + // Initialise the cache and set a verifier + OAuthCacheInit() + if tt.expectedError != errNotFound { + OAuthCache.Set(getCacheKey(tt.ipAddress), "valid-verifier", cache.DefaultExpiration) + } + + ctx := context.Background() + user, err := OAuthReturn(ctx, tt.code, tt.ipAddress) + + if tt.expectedError != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedError.Error(), err.Error()) + } else { + assert.NoError(t, err) + assert.NotNil(t, user) + } + }) + } +} diff --git a/backend/internal/entity/capability.go b/backend/internal/entity/capability.go new file mode 100644 index 000000000..8b9d7f08a --- /dev/null +++ b/backend/internal/entity/capability.go @@ -0,0 +1,11 @@ +package entity + +// Capability is the db model +type Capability struct { + Name string `json:"name" gorm:"column:name;primaryKey" filter:"name,string"` +} + +// TableName overrides the table name used by gorm +func (Capability) TableName() string { + return "capability" +} diff --git a/backend/internal/entity/certificate/methods.go b/backend/internal/entity/certificate/methods.go new file mode 100644 index 000000000..bf76976c1 --- /dev/null +++ b/backend/internal/entity/certificate/methods.go @@ -0,0 +1,97 @@ +package certificate + +import ( + "npm/internal/database" + "npm/internal/entity" + "npm/internal/jobqueue" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByStatus will select rows that are ready for requesting +func GetByStatus(status string) ([]Model, error) { + items := make([]Model, 0) + db := database.GetDB() + result := db. + Joins("INNER JOIN certificate_authority ON certificate_authority.id = certificate.certificate_authority_id AND certificate_authority.is_deleted = ?", 0). + Where("type IN ?", []string{"http", "dns"}). + Where("status = ?", status). + Where("certificate_authority_id > ?", 0). + Find(&items) + return items, result.Error +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + if expand != nil { + for idx := range items { + expandErr := items[idx].Expand(expand) + if expandErr != nil { + logger.Error("CertificatesExpansionError", expandErr) + } + } + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} + +// AddPendingJobs is intended to be used at startup to add +// anything pending to the JobQueue just once, based on +// the database row status +func AddPendingJobs() { + rows, err := GetByStatus(StatusReady) + if err != nil { + logger.Error("AddPendingJobsError", err) + return + } + + for _, row := range rows { + logger.Debug("Adding RequestCertificate job: %+v", row) + err := jobqueue.AddJob(jobqueue.Job{ + Name: "RequestCertificate", + Action: row.Request, + }) + if err != nil { + logger.Error("AddPendingJobsError", err) + } + } +} diff --git a/backend/internal/entity/certificate/model.go b/backend/internal/entity/certificate/model.go new file mode 100644 index 000000000..589814fa2 --- /dev/null +++ b/backend/internal/entity/certificate/model.go @@ -0,0 +1,313 @@ +package certificate + +import ( + "fmt" + "os" + "regexp" + "strings" + "time" + + "npm/internal/acme" + "npm/internal/config" + "npm/internal/database" + "npm/internal/entity/certificateauthority" + "npm/internal/entity/dnsprovider" + "npm/internal/entity/user" + "npm/internal/logger" + "npm/internal/model" + "npm/internal/serverevents" + "npm/internal/types" + "npm/internal/util" + + "github.com/rotisserie/eris" +) + +const ( + // TypeCustom custom cert type + TypeCustom = "custom" + // TypeHTTP http cert type + TypeHTTP = "http" + // TypeDNS dns cert type + TypeDNS = "dns" + // TypeMkcert mkcert cert type + 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 model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + CertificateAuthorityID types.NullableDBUint `json:"certificate_authority_id" gorm:"column:certificate_authority_id" filter:"certificate_authority_id,integer"` + DNSProviderID types.NullableDBUint `json:"dns_provider_id" gorm:"column:dns_provider_id" filter:"dns_provider_id,integer"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` + ExpiresOn int64 `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"` + Status string `json:"status" gorm:"column:status" filter:"status,string"` + ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` + Meta types.JSONB `json:"-" gorm:"column:meta"` + IsECC bool `json:"is_ecc" gorm:"column:is_ecc" filter:"is_ecc,bool"` + // Expansions: + CertificateAuthority *certificateauthority.Model `json:"certificate_authority,omitempty" gorm:"-"` + DNSProvider *dnsprovider.Model `json:"dns_provider,omitempty" gorm:"-"` + User *user.Model `json:"user,omitempty" gorm:"-"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "certificate" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + if !m.Validate() { + return eris.Errorf("Certificate data is incorrect or incomplete for this type") + } + + if !m.ValidateWildcardSupport() { + return eris.Errorf("Cannot use Wildcard domains with this CA") + } + + m.setDefaultStatus() + + // ensure name is trimmed of whitespace + m.Name = strings.TrimSpace(m.Name) + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil + + // todo: delete from acme.sh as well +} + +// 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.Uint == 0 && m.CertificateAuthorityID.Uint == 0 + + case TypeHTTP: + return m.DNSProviderID.Uint == 0 && m.CertificateAuthorityID.Uint > 0 + + case TypeDNS: + return m.DNSProviderID.Uint > 0 && m.CertificateAuthorityID.Uint > 0 + + case TypeMkcert: + return true + + default: + return false + } +} + +// ValidateWildcardSupport will ensure that the CA given supports wildcards, +// only if the domains on this object have at least 1 wildcard +func (m *Model) ValidateWildcardSupport() bool { + domains, err := m.DomainNames.AsStringArray() + if err != nil { + logger.Error("ValidateWildcardSupportError", err) + return false + } + + hasWildcard := false + for _, domain := range domains { + if strings.Contains(domain, "*") { + hasWildcard = true + } + } + + if hasWildcard { + // nolint: errcheck, gosec + m.Expand([]string{"certificate-authority", "dns-provider"}) + if !m.CertificateAuthority.IsWildcardSupported { + return false + } + } + + return true +} + +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(items []string) error { + var err error + + if util.SliceContainsItem(items, "certificate-authority") && m.CertificateAuthorityID.Uint > 0 { + var certificateAuthority certificateauthority.Model + certificateAuthority, err = certificateauthority.GetByID(m.CertificateAuthorityID.Uint) + m.CertificateAuthority = &certificateAuthority + } + + if util.SliceContainsItem(items, "dns-provider") && m.DNSProviderID.Uint > 0 { + var dnsProvider dnsprovider.Model + dnsProvider, err = dnsprovider.GetByID(m.DNSProviderID.Uint) + m.DNSProvider = &dnsProvider + } + + if util.SliceContainsItem(items, "user") && m.ID > 0 { + var usr user.Model + usr, err = user.GetByID(m.UserID) + m.User = &usr + } + + return err +} + +// GetCertificateLocations will return the paths on disk where the SSL +// certs should or would be. +// Returns: (key, fullchain, certFolder) +func (m *Model) GetCertificateLocations() (string, string, string) { + if m.ID == 0 { + logger.Error("GetCertificateLocationsError", eris.New("GetCertificateLocations called before certificate was saved")) + return "", "", "" + } + + certFolder := fmt.Sprintf("%s/certificates", config.Configuration.DataFolder) + + // Generate a unique folder name for this cert + m1 := regexp.MustCompile(`[^A-Za-z0-9\.]`) + + niceName := m1.ReplaceAllString(m.Name, "_") + if len(niceName) > 20 { + niceName = niceName[:20] + } + folderName := fmt.Sprintf("%d-%s", m.ID, niceName) + + return fmt.Sprintf("%s/%s/key.pem", certFolder, folderName), + fmt.Sprintf("%s/%s/fullchain.pem", certFolder, folderName), + fmt.Sprintf("%s/%s", certFolder, folderName) +} + +// Request makes a certificate request +func (m *Model) Request() error { + logger.Info("Requesting certificate for: #%d %v", m.ID, m.Name) + serverevents.SendChange("certificates") + + // nolint: errcheck, gosec + m.Expand([]string{"certificate-authority", "dns-provider"}) + m.Status = StatusRequesting + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + + // do request + domains, err := m.DomainNames.AsStringArray() + if err != nil { + logger.Error("CertificateRequestError", err) + return err + } + + certKeyFile, certFullchainFile, certFolder := m.GetCertificateLocations() + + // ensure certFolder is created + // nolint: gosec + if err := os.MkdirAll(certFolder, os.ModePerm); err != nil { + logger.Error("CreateFolderError", err) + return err + } + + errMsg, err := acme.RequestCert(domains, m.Type, certFullchainFile, certKeyFile, m.DNSProvider, m.CertificateAuthority, true) + if err != nil { + m.Status = StatusFailed + m.ErrorMessage = errMsg + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + return nil + } + + // If done + m.Status = StatusProvided + m.ExpiresOn = time.Now().UnixMilli() + if err := m.Save(); err != nil { + logger.Error("CertificateSaveError", err) + return err + } + + serverevents.SendChange("certificates") + logger.Info("Request for certificate for: #%d %v was completed", m.ID, m.Name) + return nil +} + +// GetTemplate will convert the Model to a Template +func (m *Model) GetTemplate() Template { + if m.ID == 0 { + // No or empty certificate object, happens when the host has no cert + return Template{} + } + + domainNames, _ := m.DomainNames.AsStringArray() + + return Template{ + ID: m.ID, + CreatedAt: fmt.Sprintf("%d", m.CreatedAt), // todo: nice date string + UpdatedAt: fmt.Sprintf("%d", m.UpdatedAt), // todo: nice date string + ExpiresOn: util.UnixMilliToNiceFormat(m.ExpiresOn), + Type: m.Type, + UserID: m.UserID, + CertificateAuthorityID: m.CertificateAuthorityID.Uint, + DNSProviderID: m.DNSProviderID.Uint, + Name: m.Name, + DomainNames: domainNames, + Status: m.Status, + IsECC: m.IsECC, + // These are helpers for template generation + IsCustom: m.Type == TypeCustom, + IsAcme: m.Type != TypeCustom, + IsProvided: m.ID > 0 && m.Status == StatusProvided, + Folder: m.GetFolder(), + } +} + +// GetFolder returns the folder where these certs should exist +func (m *Model) GetFolder() string { + if m.Type == TypeCustom { + return fmt.Sprintf("%s/custom_ssl/npm-%d", config.Configuration.DataFolder, m.ID) + } + return fmt.Sprintf("%s/npm-%d", config.Configuration.Acmesh.CertHome, m.ID) +} diff --git a/backend/internal/entity/certificate/template.go b/backend/internal/entity/certificate/template.go new file mode 100644 index 000000000..2e50257fd --- /dev/null +++ b/backend/internal/entity/certificate/template.go @@ -0,0 +1,22 @@ +package certificate + +// Template is the model given to the template parser, converted from the Model +type Template struct { + ID uint + CreatedAt string + UpdatedAt string + ExpiresOn string + Type string + UserID uint + CertificateAuthorityID uint + DNSProviderID uint + Name string + DomainNames []string + Status string + IsECC bool + // These are helpers for template generation + IsCustom bool + IsAcme bool // non-custom + IsProvided bool + Folder string +} diff --git a/backend/internal/entity/certificateauthority/entity_test.go b/backend/internal/entity/certificateauthority/entity_test.go new file mode 100644 index 000000000..3aa52f03d --- /dev/null +++ b/backend/internal/entity/certificateauthority/entity_test.go @@ -0,0 +1,233 @@ +package certificateauthority + +import ( + "regexp" + "testing" + + "npm/internal/errors" + "npm/internal/model" + "npm/internal/test" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + testCA *sqlmock.Rows + listCountRows *sqlmock.Rows + listRows *sqlmock.Rows +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.testCA = sqlmock.NewRows([]string{ + "id", + "name", + "acmesh_server", + "ca_bundle", + "is_wildcard_supported", + "max_domains", + }).AddRow( + 10, + "Test CA", + "https://ca.internal/acme/acme/directory", + "/etc/ssl/certs/NginxProxyManager.crt", + true, + 2, + ) + + s.listCountRows = sqlmock.NewRows([]string{ + "count(*)", + }).AddRow( + 2, + ) + + s.listRows = sqlmock.NewRows([]string{ + "id", + "name", + "acmesh_server", + "ca_bundle", + "is_wildcard_supported", + "max_domains", + }).AddRow( + 10, + "Test CA", + "https://ca.internal/acme/acme/directory", + "/etc/ssl/certs/NginxProxyManager.crt", + true, + 2, + ).AddRow( + 11, + "Test CA 2", + "https://ca2.internal/acme/acme/directory", + "/etc/ssl/certs/NginxProxyManager.crt", + true, + 5, + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +func assertModel(t *testing.T, m Model) { + assert.Equal(t, uint(10), m.ID) + assert.Equal(t, "Test CA", m.Name) + assert.Equal(t, "https://ca.internal/acme/acme/directory", m.AcmeshServer) + assert.Equal(t, "/etc/ssl/certs/NginxProxyManager.crt", m.CABundle) + assert.Equal(t, 2, m.MaxDomains) + assert.Equal(t, true, m.IsWildcardSupported) + assert.Equal(t, false, m.IsReadonly) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "certificate_authority" WHERE "certificate_authority"."id" = $1 AND "certificate_authority"."is_deleted" = $2 ORDER BY "certificate_authority"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.testCA) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestList() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "certificate_authority" WHERE "certificate_authority"."name" LIKE $1 AND "certificate_authority"."is_deleted" = $2`)). + WithArgs("%test%", 0). + WillReturnRows(s.listCountRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "certificate_authority" WHERE "certificate_authority"."name" LIKE $1 AND "certificate_authority"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)). + WithArgs("%test%", 0, 8). + WillReturnRows(s.listRows) + + p := model.PageInfo{ + Offset: 0, + Limit: 8, + Sort: []model.Sort{ + { + Field: "name", + Direction: "asc", + }, + }, + } + + f := []model.Filter{ + { + Field: "name", + Modifier: "contains", + Value: []string{"test"}, + }, + } + + resp, err := List(p, f) + require.NoError(s.T(), err) + assert.Equal(s.T(), int64(2), resp.Total) + assert.Equal(s.T(), p.Offset, resp.Offset) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Sort, resp.Sort) + assert.Equal(s.T(), f, resp.Filter) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSave() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "certificate_authority" ("created_at","updated_at","is_deleted","name","acmesh_server","ca_bundle","max_domains","is_wildcard_supported","is_readonly") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + "Test CA", + "https://ca.internal/acme/acme/directory", + "/etc/ssl/certs/NginxProxyManager.crt", + 2, + true, + false, + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + m := Model{ + Name: "Test CA", + AcmeshServer: "https://ca.internal/acme/acme/directory", + CABundle: "/etc/ssl/certs/NginxProxyManager.crt", + MaxDomains: 2, + IsWildcardSupported: true, + } + err := m.Save() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestDelete() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`UPDATE "certificate_authority" SET "is_deleted"=$1 WHERE "certificate_authority"."id" = $2 AND "certificate_authority"."is_deleted" = $3`)). + WithArgs(1, 10, 0). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + m := Model{} + err := m.Delete() + assert.Equal(s.T(), "Unable to delete a new object", err.Error()) + + m2 := Model{ + Base: model.Base{ + ID: 10, + }, + } + err2 := m2.Delete() + require.NoError(s.T(), err2) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestCheck() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + m := Model{} + err := m.Check() + assert.Nil(s.T(), err) + + m.CABundle = "/tmp/doesnotexist" + err = m.Check() + assert.Equal(s.T(), errors.ErrCABundleDoesNotExist.Error(), err.Error()) +} diff --git a/backend/internal/entity/certificateauthority/methods.go b/backend/internal/entity/certificateauthority/methods.go new file mode 100644 index 000000000..d97f12f5a --- /dev/null +++ b/backend/internal/entity/certificateauthority/methods.go @@ -0,0 +1,50 @@ +package certificateauthority + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + 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 000000000..a370a59a5 --- /dev/null +++ b/backend/internal/entity/certificateauthority/model.go @@ -0,0 +1,66 @@ +package certificateauthority + +import ( + "os" + "path/filepath" + + "npm/internal/database" + "npm/internal/errors" + "npm/internal/model" + + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + Name string `json:"name" gorm:"column:name" filter:"name,string"` + AcmeshServer string `json:"acmesh_server" gorm:"column:acmesh_server" filter:"acmesh_server,string"` + CABundle string `json:"ca_bundle" gorm:"column:ca_bundle" filter:"ca_bundle,string"` + MaxDomains int `json:"max_domains" gorm:"column:max_domains" filter:"max_domains,integer"` + IsWildcardSupported bool `json:"is_wildcard_supported" gorm:"column:is_wildcard_supported" filter:"is_wildcard_supported,boolean"` + IsReadonly bool `json:"is_readonly" gorm:"column:is_readonly" filter:"is_readonly,boolean"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "certificate_authority" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark a row as deleted +func (m *Model) Delete() error { + if m.ID == 0 { + // Can't delete a new object + return eris.New("Unable to delete a new object") + } + db := database.GetDB() + result := db.Delete(m) + return result.Error +} + +// Check will ensure the ca bundle path exists if it's set +func (m *Model) Check() error { + var err error + + if m.CABundle != "" { + if _, fileerr := os.Stat(filepath.Clean(m.CABundle)); eris.Is(fileerr, os.ErrNotExist) { + err = errors.ErrCABundleDoesNotExist + } + } + + return err +} diff --git a/backend/internal/entity/dnsprovider/entity_test.go b/backend/internal/entity/dnsprovider/entity_test.go new file mode 100644 index 000000000..40c34eabe --- /dev/null +++ b/backend/internal/entity/dnsprovider/entity_test.go @@ -0,0 +1,306 @@ +package dnsprovider + +import ( + "encoding/json" + "regexp" + "testing" + + "npm/internal/model" + "npm/internal/test" + "npm/internal/types" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + singleRow *sqlmock.Rows + listCountRows *sqlmock.Rows + listRows *sqlmock.Rows +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.singleRow = sqlmock.NewRows([]string{ + "id", + "user_id", + "name", + "acmesh_name", + "dns_sleep", + "meta", + }).AddRow( + 10, + 100, + "Route53", + "dns_aws", + 10, + getMeta().Encoded, + ) + + s.listCountRows = sqlmock.NewRows([]string{ + "count(*)", + }).AddRow( + 2, + ) + + s.listRows = sqlmock.NewRows([]string{ + "id", + "user_id", + "name", + "acmesh_name", + "dns_sleep", + "meta", + }).AddRow( + 10, + 100, + "Route53", + "dns_aws", + 10, + getMeta().Encoded, + ).AddRow( + 11, + 100, + "ClouDNS", + "dns_cloudns", + 8, + types.JSONB{}, + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +func getMeta() types.JSONB { + m := types.JSONB{} + m.UnmarshalJSON([]byte(`{"access_key_id": "BKINOTLNEREALYBL52W2I", "access_key": "NOTAREALKEY+9qSca7R9U6vUuetR8sh"}`)) + return m +} + +func assertModel(t *testing.T, m Model) { + assert.Equal(t, uint(10), m.ID, "ID not expected value") + assert.Equal(t, uint(100), m.UserID, "UserID not expected value") + assert.Equal(t, "Route53", m.Name, "Name not expected value") + assert.Equal(t, "dns_aws", m.AcmeshName, "AcmeshName not expected value") + assert.Equal(t, 10, m.DNSSleep, "DNSSleep not expected value") + assert.Equal(t, getMeta(), m.Meta, "Meta not expected value") +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE "dns_provider"."id" = $1 AND "dns_provider"."is_deleted" = $2 ORDER BY "dns_provider"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestGetAcmeShEnvVars() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type want struct { + envs []string + err error + } + + tests := []struct { + name string + dnsProvider Model + metaJSON string + want want + }{ + { + name: "dns_aws", + dnsProvider: Model{ + AcmeshName: "dns_aws", + }, + metaJSON: `{"AWS_ACCESS_KEY_ID":"sdfsdfsdfljlbjkljlkjsdfoiwje","AWS_SECRET_ACCESS_KEY":"xxxxxxx"}`, + want: want{ + envs: []string{ + `AWS_ACCESS_KEY_ID=sdfsdfsdfljlbjkljlkjsdfoiwje`, + `AWS_SECRET_ACCESS_KEY=xxxxxxx`, + }, + err: nil, + }, + }, + { + name: "dns_cf", + dnsProvider: Model{ + AcmeshName: "dns_cf", + }, + metaJSON: `{"CF_Key":"sdfsdfsdfljlbjkljlkjsdfoiwje","CF_Email":"me@example.com","CF_Token":"dkfjghdk","CF_Account_ID":"hgbdjfg","CF_Zone_ID":"ASDASD"}`, + want: want{ + envs: []string{ + `CF_Token=dkfjghdk`, + `CF_Account_ID=hgbdjfg`, + `CF_Zone_ID=ASDASD`, + `CF_Key=sdfsdfsdfljlbjkljlkjsdfoiwje`, + `CF_Email=me@example.com`, + }, + err: nil, + }, + }, + { + name: "dns_duckdns", + dnsProvider: Model{ + AcmeshName: "dns_duckdns", + }, + metaJSON: `{"DuckDNS_Token":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}`, + want: want{ + envs: []string{ + `DuckDNS_Token=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`, + }, + err: nil, + }, + }, + } + + for _, tt := range tests { + s.T().Run(tt.name, func(t *testing.T) { + var meta types.JSONB + err := json.Unmarshal([]byte(tt.metaJSON), &meta.Decoded) + assert.Equal(t, nil, err) + tt.dnsProvider.Meta = meta + envs, err := tt.dnsProvider.GetAcmeShEnvVars() + assert.Equal(t, tt.want.err, err) + for _, i := range tt.want.envs { + assert.Contains(t, envs, i) + } + }) + } +} + +func (s *testsuite) TestList() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2`)). + WithArgs("dns%", 0). + WillReturnRows(s.listCountRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "dns_provider" WHERE "dns_provider"."acmesh_name" LIKE $1 AND "dns_provider"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)). + WithArgs("dns%", 0, 8). + WillReturnRows(s.listRows) + + p := model.PageInfo{ + Offset: 0, + Limit: 8, + Sort: []model.Sort{ + { + Field: "name", + Direction: "asc", + }, + }, + } + + f := []model.Filter{ + { + Field: "acmesh_name", + Modifier: "starts", + Value: []string{"dns"}, + }, + } + + resp, err := List(p, f) + require.NoError(s.T(), err) + assert.Equal(s.T(), int64(2), resp.Total) + assert.Equal(s.T(), p.Offset, resp.Offset) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Sort, resp.Sort) + assert.Equal(s.T(), f, resp.Filter) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSave() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "dns_provider" ("created_at","updated_at","is_deleted","user_id","name","acmesh_name","dns_sleep","meta") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + 100, + "Route53", + "dns_route53", + 10, + sqlmock.AnyArg(), + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + // New model, no user + m := Model{ + Name: "Route53", + AcmeshName: "dns_route53", + DNSSleep: 10, + Meta: getMeta(), + } + err := m.Save() + assert.Equal(s.T(), "User ID must be specified", err.Error()) + + // Success + m.UserID = 100 + err = m.Save() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestDelete() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`UPDATE "dns_provider" SET "is_deleted"=$1 WHERE "dns_provider"."id" = $2 AND "dns_provider"."is_deleted" = $3`)). + WithArgs(1, 10, 0). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + m := Model{} + err := m.Delete() + assert.Equal(s.T(), "Unable to delete a new object", err.Error()) + + m2 := Model{ + Base: model.Base{ + ID: 10, + }, + } + err2 := m2.Delete() + require.NoError(s.T(), err2) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} diff --git a/backend/internal/entity/dnsprovider/methods.go b/backend/internal/entity/dnsprovider/methods.go new file mode 100644 index 000000000..450f15e8e --- /dev/null +++ b/backend/internal/entity/dnsprovider/methods.go @@ -0,0 +1,50 @@ +package dnsprovider + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a row by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of certificates +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + 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 000000000..e128922b9 --- /dev/null +++ b/backend/internal/entity/dnsprovider/model.go @@ -0,0 +1,88 @@ +package dnsprovider + +import ( + "fmt" + + "npm/internal/database" + "npm/internal/dnsproviders" + "npm/internal/logger" + "npm/internal/model" + "npm/internal/types" + + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + AcmeshName string `json:"acmesh_name" gorm:"column:acmesh_name" filter:"acmesh_name,string"` + DNSSleep int `json:"dns_sleep" gorm:"column:dns_sleep" filter:"dns_sleep,integer"` + Meta types.JSONB `json:"meta" gorm:"column:meta"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "dns_provider" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark a row as deleted +func (m *Model) Delete() error { + if m.ID == 0 { + // Can't delete a new object + return eris.New("Unable to delete a new object") + } + db := database.GetDB() + result := db.Delete(m) + return result.Error +} + +// GetAcmeShEnvVars returns the env vars required for acme.sh dns cert requests +func (m *Model) GetAcmeShEnvVars() ([]string, error) { + // First, fetch the provider obj with this AcmeShName + _, err := dnsproviders.Get(m.AcmeshName) + if err != nil { + logger.Error("GetAcmeShEnvVarsError", err) + return nil, err + } + + // Convert the meta interface to envs slice for use by acme.sh + envs := getEnvsFromMeta(m.Meta.Decoded) + return envs, nil +} + +func getEnvsFromMeta(meta any) []string { + if rec, ok := meta.(map[string]any); ok { + envs := make([]string, 0) + for key, val := range rec { + if f, ok := val.(string); ok { + envs = append(envs, fmt.Sprintf(`%s=%v`, key, f)) + } else if f, ok := val.(int); ok { + envs = append(envs, fmt.Sprintf(`%s=%d`, key, f)) + } + } + return envs + } + + logger.Debug("getEnvsFromMeta: meta is not an map of strings") + return nil +} diff --git a/backend/internal/entity/filters.go b/backend/internal/entity/filters.go new file mode 100644 index 000000000..906a25e54 --- /dev/null +++ b/backend/internal/entity/filters.go @@ -0,0 +1,19 @@ +package entity + +import ( + "npm/internal/model" + "npm/internal/tags" +) + +// GetFilterMap returns the filter map +// _ was called `includeBaseEntity` +func GetFilterMap(m any, _ bool) map[string]model.FilterMapValue { + filterMap := tags.GetFilterMap(m, "") + + // TODO: this is done in GetFilterMap isn't it? + // if includeBaseEntity { + // return mergeFilterMaps(tags.GetFilterMap(model.Base{}, ""), filterMap) + // } + + return filterMap +} diff --git a/backend/internal/entity/host/entity_test.go b/backend/internal/entity/host/entity_test.go new file mode 100644 index 000000000..031ec860f --- /dev/null +++ b/backend/internal/entity/host/entity_test.go @@ -0,0 +1,233 @@ +package host + +import ( + "regexp" + "testing" + "time" + + "npm/internal/model" + "npm/internal/status" + "npm/internal/test" + "npm/internal/types" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + singleRow *sqlmock.Rows +} + +func makeJSONB(str string) types.JSONB { + m := types.JSONB{} + m.UnmarshalJSON([]byte(str)) + return m +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.singleRow = sqlmock.NewRows([]string{ + "id", + "user_id", + "type", + "nginx_template_id", + "listen_interface", + "domain_names", + "upstream_id", + "proxy_scheme", + "proxy_host", + "proxy_port", + "certificate_id", + "access_list_id", + "ssl_forced", + "caching_enabled", + "block_exploits", + "allow_websocket_upgrade", + "http2_support", + "hsts_enabled", + "hsts_subdomains", + "paths", + "advanced_config", + "status", + "error_message", + "is_disabled", + }).AddRow( + 10, // ID + 100, // UserID + "proxy", // Type + 20, // NginxTemplateID + "", // ListenInterface + makeJSONB("[\"example.com\"]"), // DomainNames + 0, // UpstreamID + "http", // ProxyScheme + "127.0.0.1", // ProxyHost + 3000, // ProxyPort + types.NullableDBUint{Uint: 0}, // CertificateID + types.NullableDBUint{Uint: 0}, // AccessListID + false, // SSLForced + false, // CachingEnabled + false, // BlockExploits + false, // AllowWebsocketUpgrade + false, // HTTP2Support + false, // HSTSEnabled + false, // HSTSSubdomains + "", // Paths + "", // AdvancedConfig + status.StatusReady, // Status + "", // ErrorMessage + false, // IsDisabled + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "host" WHERE "host"."id" = $1 AND "host"."is_deleted" = $2 ORDER BY "host"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assert.Equal(s.T(), uint(10), m.ID) +} + +func (s *testsuite) TestSave() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "host" ("created_at","updated_at","is_deleted","user_id","type","nginx_template_id","listen_interface","domain_names","upstream_id","proxy_scheme","proxy_host","proxy_port","certificate_id","access_list_id","ssl_forced","caching_enabled","block_exploits","allow_websocket_upgrade","http2_support","hsts_enabled","hsts_subdomains","paths","advanced_config","status","error_message","is_disabled") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + 100, + "proxy", + 20, + "", + "[\"example.com\"]", + nil, + "http", + "127.0.0.1", + 3000, // proxy_port + nil, + nil, + false, + false, + false, + false, + false, + false, + false, + "", + "", + status.StatusReady, + "", + false, + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + // New model, as system + m := Model{ + UserID: 100, + Type: "proxy", + NginxTemplateID: 20, + DomainNames: makeJSONB("[\"example.com\"]"), + ProxyScheme: "http", + ProxyHost: "127.0.0.1", + ProxyPort: 3000, + Status: status.StatusReady, + } + err := m.Save(true) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestDelete() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`UPDATE "host" SET "is_deleted"=$1 WHERE "host"."id" = $2 AND "host"."is_deleted" = $3`)). + WithArgs(1, 10, 0). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + m := Model{} + err := m.Delete() + assert.Equal(s.T(), "Unable to delete a new object", err.Error()) + + m2 := Model{ + Base: model.Base{ + ID: 10, + }, + } + err2 := m2.Delete() + require.NoError(s.T(), err2) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGetTemplate() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + m := Model{ + Base: model.Base{ + ID: 10, + CreatedAt: time.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), + UpdatedAt: time.Date(2018, 8, 12, 7, 30, 24, 16, time.UTC).UnixMilli(), + }, + UserID: 100, + Type: "proxy", + NginxTemplateID: 20, + DomainNames: makeJSONB("[\"example.com\"]"), + ProxyScheme: "http", + ProxyHost: "127.0.0.1", + ProxyPort: 3000, + Status: status.StatusReady, + } + + t := m.GetTemplate() + assert.Equal(s.T(), uint(10), t.ID) + assert.Equal(s.T(), "Mon, 01 Jan 2018 10:00:00 AEST", t.CreatedAt) + assert.Equal(s.T(), "Sun, 12 Aug 2018 17:30:24 AEST", t.UpdatedAt) + assert.Equal(s.T(), uint(100), t.UserID) + assert.Equal(s.T(), "proxy", t.Type) + assert.Equal(s.T(), uint(20), t.NginxTemplateID) + assert.Equal(s.T(), "http", t.ProxyScheme) + assert.Equal(s.T(), "127.0.0.1", t.ProxyHost) + assert.Equal(s.T(), 3000, t.ProxyPort) + assert.Equal(s.T(), []string{"example.com"}, t.DomainNames) + assert.Equal(s.T(), status.StatusReady, t.Status) +} diff --git a/backend/internal/entity/host/methods.go b/backend/internal/entity/host/methods.go new file mode 100644 index 000000000..b73f6f017 --- /dev/null +++ b/backend/internal/entity/host/methods.go @@ -0,0 +1,94 @@ +package host + +import ( + "npm/internal/database" + "npm/internal/entity" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Host by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "domain_names", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + if expand != nil { + for idx := range items { + expandErr := items[idx].Expand(expand) + if expandErr != nil { + logger.Error("HostsExpansionError", expandErr) + } + } + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} + +// GetUpstreamUseCount returns the number of hosts that are using +// an upstream, and have not been deleted. +func GetUpstreamUseCount(upstreamID uint) int64 { + db := database.GetDB() + + var count int64 + if result := db.Model(&Model{}).Where("upstream_id = ?", upstreamID).Count(&count); result.Error != nil { + logger.Debug("GetUpstreamUseCount Error: %v", result.Error) + return 0 + } + return count +} + +// GetCertificateUseCount returns the number of hosts that are using +// a certificate, and have not been deleted. +func GetCertificateUseCount(certificateID uint) int64 { + db := database.GetDB() + + var count int64 + if result := db.Model(&Model{}).Where("certificate_id = ?", certificateID).Count(&count); result.Error != nil { + logger.Debug("GetUpstreamUseCount Error: %v", result.Error) + return 0 + } + return count +} + +// AddPendingJobs is intended to be used at startup to add +// anything pending to the JobQueue just once, based on +// the database row status +func AddPendingJobs() { + // todo +} diff --git a/backend/internal/entity/host/model.go b/backend/internal/entity/host/model.go new file mode 100644 index 000000000..8907eda2c --- /dev/null +++ b/backend/internal/entity/host/model.go @@ -0,0 +1,172 @@ +package host + +import ( + "time" + + "npm/internal/database" + "npm/internal/entity/certificate" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" + "npm/internal/entity/user" + "npm/internal/model" + "npm/internal/status" + "npm/internal/types" + "npm/internal/util" + + "github.com/rotisserie/eris" +) + +const ( + // ProxyHostType is self explanatory + ProxyHostType = "proxy" + // RedirectionHostType is self explanatory + RedirectionHostType = "redirection" + // DeadHostType is self explanatory + DeadHostType = "dead" +) + +// Model is the model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"` + ListenInterface string `json:"listen_interface" gorm:"column:listen_interface" filter:"listen_interface,string"` + DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` + UpstreamID types.NullableDBUint `json:"upstream_id" gorm:"column:upstream_id" filter:"upstream_id,integer"` + ProxyScheme string `json:"proxy_scheme" gorm:"column:proxy_scheme" filter:"proxy_scheme,string"` + ProxyHost string `json:"proxy_host" gorm:"column:proxy_host" filter:"proxy_host,string"` + ProxyPort int `json:"proxy_port" gorm:"column:proxy_port" filter:"proxy_port,integer"` + CertificateID types.NullableDBUint `json:"certificate_id" gorm:"column:certificate_id" filter:"certificate_id,integer"` + AccessListID types.NullableDBUint `json:"access_list_id" gorm:"column:access_list_id" filter:"access_list_id,integer"` + SSLForced bool `json:"ssl_forced" gorm:"column:ssl_forced" filter:"ssl_forced,boolean"` + CachingEnabled bool `json:"caching_enabled" gorm:"column:caching_enabled" filter:"caching_enabled,boolean"` + BlockExploits bool `json:"block_exploits" gorm:"column:block_exploits" filter:"block_exploits,boolean"` + AllowWebsocketUpgrade bool `json:"allow_websocket_upgrade" gorm:"column:allow_websocket_upgrade" filter:"allow_websocket_upgrade,boolean"` + HTTP2Support bool `json:"http2_support" gorm:"column:http2_support" filter:"http2_support,boolean"` + HSTSEnabled bool `json:"hsts_enabled" gorm:"column:hsts_enabled" filter:"hsts_enabled,boolean"` + HSTSSubdomains bool `json:"hsts_subdomains" gorm:"column:hsts_subdomains" filter:"hsts_subdomains,boolean"` + Paths string `json:"paths" gorm:"column:paths" filter:"paths,string"` + AdvancedConfig string `json:"advanced_config" gorm:"column:advanced_config" filter:"advanced_config,string"` + Status string `json:"status" gorm:"column:status" filter:"status,string"` + ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` + IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` + // Expansions + Certificate *certificate.Model `json:"certificate,omitempty" gorm:"-"` + NginxTemplate *nginxtemplate.Model `json:"nginx_template,omitempty" gorm:"-"` + User *user.Model `json:"user,omitempty" gorm:"-"` + Upstream *upstream.Model `json:"upstream,omitempty" gorm:"-"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "host" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save(skipConfiguration bool) error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + if !skipConfiguration { + // Set this host as requiring reconfiguration + m.Status = status.StatusReady + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark a row as deleted +func (m *Model) Delete() error { + if m.ID == 0 { + // Can't delete a new object + return eris.New("Unable to delete a new object") + } + db := database.GetDB() + result := db.Delete(m) + return result.Error +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + // Always expand the upstream + if m.UpstreamID.Uint > 0 { + var u upstream.Model + u, err = upstream.GetByID(m.UpstreamID.Uint) + m.Upstream = &u + } + + if util.SliceContainsItem(items, "user") && m.ID > 0 { + var usr user.Model + usr, err = user.GetByID(m.UserID) + m.User = &usr + } + + if util.SliceContainsItem(items, "certificate") && m.CertificateID.Uint > 0 { + var cert certificate.Model + cert, err = certificate.GetByID(m.CertificateID.Uint) + m.Certificate = &cert + } + + if util.SliceContainsItem(items, "nginxtemplate") && m.NginxTemplateID > 0 { + var templ nginxtemplate.Model + templ, err = nginxtemplate.GetByID(m.NginxTemplateID) + m.NginxTemplate = &templ + } + + if util.SliceContainsItem(items, "upstream") && m.UpstreamID.Uint > 0 { + var ups upstream.Model + ups, err = upstream.GetByID(m.UpstreamID.Uint) + m.Upstream = &ups + } + + return err +} + +// GetTemplate will convert the Model to a Template +func (m *Model) GetTemplate() Template { + domainNames, _ := m.DomainNames.AsStringArray() + + t := Template{ + ID: m.ID, + CreatedAt: time.UnixMilli(m.CreatedAt).Format(time.RFC1123), + UpdatedAt: time.UnixMilli(m.UpdatedAt).Format(time.RFC1123), + UserID: m.UserID, + Type: m.Type, + NginxTemplateID: m.NginxTemplateID, + ProxyScheme: m.ProxyScheme, + ProxyHost: m.ProxyHost, + ProxyPort: m.ProxyPort, + ListenInterface: m.ListenInterface, + DomainNames: domainNames, + UpstreamID: m.UpstreamID.Uint, + CertificateID: m.CertificateID.Uint, + AccessListID: m.AccessListID.Uint, + SSLForced: m.SSLForced, + CachingEnabled: m.CachingEnabled, + BlockExploits: m.BlockExploits, + AllowWebsocketUpgrade: m.AllowWebsocketUpgrade, + HTTP2Support: m.HTTP2Support, + HSTSEnabled: m.HSTSEnabled, + HSTSSubdomains: m.HSTSSubdomains, + Paths: m.Paths, + AdvancedConfig: m.AdvancedConfig, + Status: m.Status, + ErrorMessage: m.ErrorMessage, + IsDisabled: m.IsDisabled, + } + + return t +} diff --git a/backend/internal/entity/host/template.go b/backend/internal/entity/host/template.go new file mode 100644 index 000000000..640ee5d1d --- /dev/null +++ b/backend/internal/entity/host/template.go @@ -0,0 +1,34 @@ +package host + +import "npm/internal/entity/upstream" + +// Template is the model given to the template parser, converted from the Model +type Template struct { + ID uint + CreatedAt string + UpdatedAt string + UserID uint + Type string + NginxTemplateID uint + ProxyScheme string + ProxyHost string + ProxyPort int + ListenInterface string + DomainNames []string + UpstreamID uint + CertificateID uint + AccessListID uint + SSLForced bool + CachingEnabled bool + BlockExploits bool + AllowWebsocketUpgrade bool + HTTP2Support bool + HSTSEnabled bool + HSTSSubdomains bool + IsDisabled bool + Paths string + AdvancedConfig string + Status string + ErrorMessage string + Upstream upstream.Model +} diff --git a/backend/internal/entity/lists.go b/backend/internal/entity/lists.go new file mode 100644 index 000000000..186253b7f --- /dev/null +++ b/backend/internal/entity/lists.go @@ -0,0 +1,47 @@ +package entity + +import ( + "npm/internal/database" + "npm/internal/model" + + "gorm.io/gorm" +) + +// ListResponse is the JSON response for users list +type ListResponse struct { + Total int64 `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items any `json:"items,omitempty"` +} + +// ListQueryBuilder is used to setup queries for lists +func ListQueryBuilder( + _ *model.PageInfo, + filters []model.Filter, + filterMap map[string]model.FilterMapValue, +) *gorm.DB { + scopes := make([]func(*gorm.DB) *gorm.DB, 0) + scopes = append(scopes, ScopeFilters(filters, filterMap)) + return database.GetDB().Scopes(scopes...) +} + +// AddOrderToList is used after query above is used for counting +// Postgres in particular doesn't like count(*) when ordering at the same time +func AddOrderToList( + dbo *gorm.DB, + sort []model.Sort, + defaultSort model.Sort, +) *gorm.DB { + return dbo.Scopes(ScopeOrderBy(sort, defaultSort)) +} + +// AddOffsetLimitToList is used after query above is used for pagination +func AddOffsetLimitToList( + dbo *gorm.DB, + pageInfo *model.PageInfo, +) *gorm.DB { + return dbo.Scopes(ScopeOffsetLimit(pageInfo)) +} diff --git a/backend/internal/entity/nginxtemplate/methods.go b/backend/internal/entity/nginxtemplate/methods.go new file mode 100644 index 000000000..67644aec9 --- /dev/null +++ b/backend/internal/entity/nginxtemplate/methods.go @@ -0,0 +1,50 @@ +package nginxtemplate + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a Host by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "created_at", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/nginxtemplate/model.go b/backend/internal/entity/nginxtemplate/model.go new file mode 100644 index 000000000..1d04a06ad --- /dev/null +++ b/backend/internal/entity/nginxtemplate/model.go @@ -0,0 +1,51 @@ +package nginxtemplate + +import ( + "npm/internal/database" + "npm/internal/model" + + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + Template string `json:"template" gorm:"column:template" filter:"template,string"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "nginx_template" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil +} diff --git a/backend/internal/entity/scopes.go b/backend/internal/entity/scopes.go new file mode 100644 index 000000000..4c1922efe --- /dev/null +++ b/backend/internal/entity/scopes.go @@ -0,0 +1,127 @@ +package entity + +import ( + "fmt" + "strings" + + "npm/internal/database" + "npm/internal/model" + + "gorm.io/gorm" +) + +// ScopeOffsetLimit ... +func ScopeOffsetLimit(pageInfo *model.PageInfo) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if pageInfo.Offset > 0 || pageInfo.Limit > 0 { + return db.Limit(pageInfo.Limit).Offset(pageInfo.Offset) + } + return db + } +} + +// ScopeOrderBy ... +func ScopeOrderBy(sort []model.Sort, defaultSort model.Sort) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if sort != nil { + // Sort by items in slice + return db.Order(sortToOrderString(sort)) + } else if defaultSort.Field != "" { + // Default to this sort + str := defaultSort.Field + if defaultSort.Direction != "" { + str = str + " " + defaultSort.Direction + } + return db.Order(str) + } + return db + } +} + +// ScopeFilters ... +func ScopeFilters(filters []model.Filter, filterMap map[string]model.FilterMapValue) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + like := database.GetCaseInsensitiveLike() + for _, f := range filters { + // Lookup this filter field from the name map + if _, ok := filterMap[f.Field]; ok { + f.Field = filterMap[f.Field].Field + } + + // For boolean fields, the value needs tweaking + if filterMap[f.Field].Type == "boolean" { + f.Value = parseBoolValue(f.Value[0]) + } + + // Quick adjustments for commonalities + if f.Modifier == "in" && len(f.Value) == 1 { + f.Modifier = "equals" + } else if f.Modifier == "notin" && len(f.Value) == 1 { + f.Modifier = "not" + } + + switch strings.ToLower(f.Modifier) { + case "not": + db.Where(fmt.Sprintf("%s != ?", f.Field), f.Value) + case "min": + db.Where(fmt.Sprintf("%s >= ?", f.Field), f.Value) + case "max": + db.Where(fmt.Sprintf("%s <= ?", f.Field), f.Value) + case "greater": + db.Where(fmt.Sprintf("%s > ?", f.Field), f.Value) + case "lesser": + db.Where(fmt.Sprintf("%s < ?", f.Field), f.Value) + + // LIKE modifiers: + case "contains": + db.Where(fmt.Sprintf("%s %s ?", f.Field, like), `%`+f.Value[0]+`%`) + case "starts": + db.Where(fmt.Sprintf("%s %s ?", f.Field, like), f.Value[0]+`%`) + case "ends": + db.Where(fmt.Sprintf("%s %s ?", f.Field, like), `%`+f.Value[0]) + + // Array parameter modifiers: + case "in": + db.Where(fmt.Sprintf("%s IN ?", f.Field), f.Value) + case "notin": + db.Where(fmt.Sprintf("%s NOT IN ?", f.Field), f.Value) + + // Default: equals + default: + db.Where(fmt.Sprintf("%s = ?", f.Field), f.Value) + } + } + return db + } +} + +func sortToOrderString(sorts []model.Sort) string { + strs := make([]string, 0) + for _, i := range sorts { + str := i.Field + if i.Direction != "" { + str = str + " " + i.Direction + } + strs = append(strs, str) + } + return strings.Join(strs, ", ") +} + +func parseBoolValue(v string) []string { + bVal := "0" + switch strings.ToLower(v) { + case "yes": + fallthrough + case "true": + fallthrough + case "on": + fallthrough + case "t": + fallthrough + case "1": + fallthrough + case "y": + bVal = "1" + } + return []string{bVal} +} diff --git a/backend/internal/entity/scopes_test.go b/backend/internal/entity/scopes_test.go new file mode 100644 index 000000000..b0c61813e --- /dev/null +++ b/backend/internal/entity/scopes_test.go @@ -0,0 +1,33 @@ +package entity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseBoolValue(t *testing.T) { + tests := []struct { + input string + expected []string + }{ + {"yes", []string{"1"}}, + {"true", []string{"1"}}, + {"on", []string{"1"}}, + {"t", []string{"1"}}, + {"1", []string{"1"}}, + {"y", []string{"1"}}, + {"no", []string{"0"}}, + {"false", []string{"0"}}, + {"off", []string{"0"}}, + {"f", []string{"0"}}, + {"0", []string{"0"}}, + {"n", []string{"0"}}, + {"random", []string{"0"}}, + } + + for _, test := range tests { + result := parseBoolValue(test.input) + assert.Equal(t, test.expected, result, "Input: %s", test.input) + } +} diff --git a/backend/internal/entity/setting/auth_methods.go b/backend/internal/entity/setting/auth_methods.go new file mode 100644 index 000000000..3d54f2958 --- /dev/null +++ b/backend/internal/entity/setting/auth_methods.go @@ -0,0 +1,32 @@ +package setting + +import ( + "encoding/json" + "slices" +) + +// GetAuthMethods returns the authentication methods enabled for this site +func GetAuthMethods() ([]string, error) { + var m Model + if err := m.LoadByName("auth-methods"); err != nil { + return nil, err + } + + var r []string + if err := json.Unmarshal([]byte(m.Value.String()), &r); err != nil { + return nil, err + } + + return r, nil +} + +// AuthMethodEnabled checks that the auth method given is +// enabled in the db setting +func AuthMethodEnabled(method string) bool { + r, err := GetAuthMethods() + if err != nil { + return false + } + + return slices.Contains(r, method) +} diff --git a/backend/internal/entity/setting/ldap.go b/backend/internal/entity/setting/ldap.go new file mode 100644 index 000000000..d29e63ca1 --- /dev/null +++ b/backend/internal/entity/setting/ldap.go @@ -0,0 +1,32 @@ +package setting + +import ( + "encoding/json" +) + +// LDAPSettings are the settings for LDAP that come from +// the `ldap-auth` setting value +type LDAPSettings struct { + Host string `json:"host"` + BaseDN string `json:"base_dn"` + UserDN string `json:"user_dn"` + EmailProperty string `json:"email_property"` + NameProperty string `json:"name_property"` + SelfFilter string `json:"self_filter"` + AutoCreateUser bool `json:"auto_create_user"` +} + +// GetLDAPSettings will return the LDAP settings +func GetLDAPSettings() (LDAPSettings, error) { + var l LDAPSettings + var m Model + if err := m.LoadByName("ldap-auth"); err != nil { + return l, err + } + + if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil { + return l, err + } + + return l, nil +} diff --git a/backend/internal/entity/setting/methods.go b/backend/internal/entity/setting/methods.go new file mode 100644 index 000000000..4fa03a0b8 --- /dev/null +++ b/backend/internal/entity/setting/methods.go @@ -0,0 +1,57 @@ +package setting + +import ( + "npm/internal/entity" + "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 +} + +// List will return a list of settings +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + 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 000000000..7cf246183 --- /dev/null +++ b/backend/internal/entity/setting/model.go @@ -0,0 +1,50 @@ +package setting + +import ( + "strings" + + "npm/internal/database" + "npm/internal/model" + + "gorm.io/datatypes" +) + +// Model is the model +type Model struct { + model.Base + Name string `json:"name" gorm:"column:name" filter:"name,string"` + Description string `json:"description" gorm:"column:description" filter:"description,string"` + Value datatypes.JSON `json:"value" gorm:"column:value"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "setting" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// LoadByName will load from a Name +func (m *Model) LoadByName(name string) error { + db := database.GetDB() + result := db.Where("name = ?", strings.ToLower(name)).First(&m) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + // ensure name is trimmed of whitespace + m.Name = strings.ToLower(strings.TrimSpace(m.Name)) + + db := database.GetDB() + if result := db.Save(m); result.Error != nil { + return result.Error + } + + return nil +} diff --git a/backend/internal/entity/setting/oauth.go b/backend/internal/entity/setting/oauth.go new file mode 100644 index 000000000..bfc5e0489 --- /dev/null +++ b/backend/internal/entity/setting/oauth.go @@ -0,0 +1,42 @@ +package setting + +import ( + "encoding/json" +) + +// OAuthSettings are the settings for OAuth that come from +// the `oauth-auth` setting value +type OAuthSettings struct { + AutoCreateUser bool `json:"auto_create_user"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + AuthURL string `json:"authorization_url"` + TokenURL string `json:"token_url"` + Identifier string `json:"identifier"` + LogoutURL string `json:"logout_url"` + Scopes []string `json:"scopes"` + ResourceURL string `json:"resource_url"` +} + +// GetOAuthSettings will return the OAuth settings +func GetOAuthSettings() (OAuthSettings, error) { + var o OAuthSettings + var m Model + if err := m.LoadByName("oauth-auth"); err != nil { + return o, err + } + + if err := json.Unmarshal([]byte(m.Value.String()), &o); err != nil { + return o, err + } + + o.ApplyDefaults() + return o, nil +} + +// ApplyDefaults will ensure there are defaults set +func (m *OAuthSettings) ApplyDefaults() { + if m.Identifier == "" { + m.Identifier = "email" + } +} diff --git a/backend/internal/entity/stream/methods.go b/backend/internal/entity/stream/methods.go new file mode 100644 index 000000000..d23cc8ee4 --- /dev/null +++ b/backend/internal/entity/stream/methods.go @@ -0,0 +1,50 @@ +package stream + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a auth by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of hosts +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + 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 000000000..206b91ea4 --- /dev/null +++ b/backend/internal/entity/stream/model.go @@ -0,0 +1,54 @@ +package stream + +import ( + "npm/internal/database" + "npm/internal/model" + "npm/internal/types" + + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + ExpiresOn types.DBDate `json:"expires_on" gorm:"column:expires_on" filter:"expires_on,integer"` + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Provider string `json:"provider" gorm:"column:provider" filter:"provider,string"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + DomainNames types.JSONB `json:"domain_names" gorm:"column:domain_names" filter:"domain_names,string"` + Meta types.JSONB `json:"-" gorm:"column:meta"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "stream" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil +} diff --git a/backend/internal/entity/upstream/methods.go b/backend/internal/entity/upstream/methods.go new file mode 100644 index 000000000..45f38878d --- /dev/null +++ b/backend/internal/entity/upstream/methods.go @@ -0,0 +1,56 @@ +package upstream + +import ( + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a Upstream by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// List will return a list of Upstreams +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + // Expand to get servers, at a minimum + for idx := range items { + // nolint: errcheck, gosec + items[idx].Expand(expand) + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/upstream/model.go b/backend/internal/entity/upstream/model.go new file mode 100644 index 000000000..c817d2720 --- /dev/null +++ b/backend/internal/entity/upstream/model.go @@ -0,0 +1,116 @@ +package upstream + +import ( + "strings" + + "npm/internal/database" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstreamserver" + "npm/internal/entity/user" + "npm/internal/model" + "npm/internal/status" + "npm/internal/util" + + "github.com/rotisserie/eris" +) + +// Model is the model +// See: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream +type Model struct { + model.Base + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Name string `json:"name" gorm:"column:name" filter:"name,string"` + NginxTemplateID uint `json:"nginx_template_id" gorm:"column:nginx_template_id" filter:"nginx_template_id,integer"` + IPHash bool `json:"ip_hash" gorm:"column:ip_hash" filter:"ip_hash,boolean"` + NTLM bool `json:"ntlm" gorm:"column:ntlm" filter:"ntlm,boolean"` + Keepalive int `json:"keepalive" gorm:"column:keepalive" filter:"keepalive,integer"` + KeepaliveRequests int `json:"keepalive_requests" gorm:"column:keepalive_requests" filter:"keepalive_requests,integer"` + KeepaliveTime string `json:"keepalive_time" gorm:"column:keepalive_time" filter:"keepalive_time,string"` + KeepaliveTimeout string `json:"keepalive_timeout" gorm:"column:keepalive_timeout" filter:"keepalive_timeout,string"` + AdvancedConfig string `json:"advanced_config" gorm:"column:advanced_config" filter:"advanced_config,string"` + Status string `json:"status" gorm:"column:status" filter:"status,string"` + ErrorMessage string `json:"error_message" gorm:"column:error_message" filter:"error_message,string"` + // Expansions + Servers []upstreamserver.Model `json:"servers" gorm:"-"` + NginxTemplate *nginxtemplate.Model `json:"nginx_template,omitempty" gorm:"-"` + User *user.Model `json:"user,omitempty" gorm:"-"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "upstream" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save(skipConfiguration bool) error { + if m.UserID == 0 { + return eris.Errorf("User ID must be specified") + } + + // ensure name is trimmed of whitespace + m.Name = strings.TrimSpace(m.Name) + + if !skipConfiguration { + // Set this upstream as requiring reconfiguration + m.Status = status.StatusReady + } + + db := database.GetDB() + if result := db.Save(m); result.Error != nil { + return result.Error + } + + // Save Servers + var err error + for idx := range m.Servers { + // Continue if previous iteration didn't cause an error + if err == nil { + m.Servers[idx].UpstreamID = m.ID + err = m.Servers[idx].Save() + } + } + + return err +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + // Always expand servers, if not done already + if len(m.Servers) == 0 { + m.Servers, err = upstreamserver.GetByUpstreamID(m.ID) + } + + if util.SliceContainsItem(items, "user") && m.ID > 0 { + var usr user.Model + usr, err = user.GetByID(m.UserID) + m.User = &usr + } + + if util.SliceContainsItem(items, "nginxtemplate") && m.NginxTemplateID > 0 { + var templ nginxtemplate.Model + templ, err = nginxtemplate.GetByID(m.NginxTemplateID) + m.NginxTemplate = &templ + } + + return err +} diff --git a/backend/internal/entity/upstreamserver/methods.go b/backend/internal/entity/upstreamserver/methods.go new file mode 100644 index 000000000..2aa207410 --- /dev/null +++ b/backend/internal/entity/upstreamserver/methods.go @@ -0,0 +1,59 @@ +package upstreamserver + +import ( + "npm/internal/database" + "npm/internal/entity" + "npm/internal/model" +) + +// GetByID finds a Upstream Server by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByUpstreamID finds all servers in the upstream +func GetByUpstreamID(upstreamID uint) ([]Model, error) { + items := make([]Model, 0) + db := database.GetDB() + result := db.Where("upstream_id = ?", upstreamID).Order("server ASC").Find(&items) + return items, result.Error +} + +// List will return a list of Upstreams +func List(pageInfo model.PageInfo, filters []model.Filter) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "server", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/upstreamserver/model.go b/backend/internal/entity/upstreamserver/model.go new file mode 100644 index 000000000..d572bdc71 --- /dev/null +++ b/backend/internal/entity/upstreamserver/model.go @@ -0,0 +1,48 @@ +package upstreamserver + +import ( + "npm/internal/database" + "npm/internal/model" +) + +// Model is the model +type Model struct { + model.Base + UpstreamID uint `json:"upstream_id" gorm:"column:upstream_id" filter:"upstream_id,integer"` + Server string `json:"server" gorm:"column:server" filter:"server,string"` + Weight int `json:"weight" gorm:"column:weight" filter:"weight,integer"` + MaxConns int `json:"max_conns" gorm:"column:max_conns" filter:"max_conns,integer"` + MaxFails int `json:"max_fails" gorm:"column:max_fails" filter:"max_fails,integer"` + FailTimeout int `json:"fail_timeout" gorm:"column:fail_timeout" filter:"fail_timeout,integer"` + Backup bool `json:"backup" gorm:"column:is_backup" filter:"backup,boolean"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "upstream_server" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark row as deleted +func (m *Model) Delete() bool { + if m.ID == 0 { + // Can't delete a new object + return false + } + db := database.GetDB() + result := db.Delete(m) + return result.Error == nil +} diff --git a/backend/internal/entity/user/capabilities.go b/backend/internal/entity/user/capabilities.go new file mode 100644 index 000000000..a4c74b89a --- /dev/null +++ b/backend/internal/entity/user/capabilities.go @@ -0,0 +1,40 @@ +package user + +const ( + // CapabilityFullAdmin can do anything + CapabilityFullAdmin = "full-admin" + // CapabilityAccessListsView access lists view + CapabilityAccessListsView = "access-lists.view" + // CapabilityAccessListsManage access lists manage + CapabilityAccessListsManage = "access-lists.manage" + // CapabilityAuditLogView audit log view + CapabilityAuditLogView = "audit-log.view" + // CapabilityCertificatesView certificates view + CapabilityCertificatesView = "certificates.view" + // CapabilityCertificatesManage certificates manage + CapabilityCertificatesManage = "certificates.manage" + // CapabilityCertificateAuthoritiesView certificate authorities view + CapabilityCertificateAuthoritiesView = "certificate-authorities.view" + // CapabilityCertificateAuthoritiesManage certificate authorities manage + CapabilityCertificateAuthoritiesManage = "certificate-authorities.manage" + // CapabilityDNSProvidersView dns providers view + CapabilityDNSProvidersView = "dns-providers.view" + // CapabilityDNSProvidersManage dns providers manage + CapabilityDNSProvidersManage = "dns-providers.manage" + // CapabilityHostsView hosts view + CapabilityHostsView = "hosts.view" + // CapabilityHostsManage hosts manage + CapabilityHostsManage = "hosts.manage" + // CapabilityNginxTemplatesView nginx-templates view + CapabilityNginxTemplatesView = "nginx-templates.view" + // CapabilityNginxTemplatesManage nginx-templates manage + CapabilityNginxTemplatesManage = "nginx-templates.manage" + // CapabilitySettingsManage settings manage + CapabilitySettingsManage = "settings.manage" + // CapabilityStreamsView streams view + CapabilityStreamsView = "streams.view" + // CapabilityStreamsManage streams manage + CapabilityStreamsManage = "streams.manage" + // CapabilityUsersManage users manage + CapabilityUsersManage = "users.manage" +) diff --git a/backend/internal/entity/user/entity_test.go b/backend/internal/entity/user/entity_test.go new file mode 100644 index 000000000..7045b4d08 --- /dev/null +++ b/backend/internal/entity/user/entity_test.go @@ -0,0 +1,454 @@ +package user + +import ( + goerrors "errors" + "regexp" + "testing" + + "npm/internal/errors" + "npm/internal/model" + "npm/internal/test" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + singleRow *sqlmock.Rows + capabilitiesRows *sqlmock.Rows + listCountRows *sqlmock.Rows + listRows *sqlmock.Rows +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.singleRow = sqlmock.NewRows([]string{ + "id", + "name", + "email", + "is_disabled", + "is_system", + }).AddRow( + 10, + "John Doe", + "jon@example.com", + false, + false, + ) + + s.capabilitiesRows = sqlmock.NewRows([]string{ + "user_id", + "capability_name", + }).AddRow( + 10, + "hosts.view", + ).AddRow( + 10, + "hosts.manage", + ) + + s.listCountRows = sqlmock.NewRows([]string{ + "count(*)", + }).AddRow( + 2, + ) + + s.listRows = sqlmock.NewRows([]string{ + "id", + "name", + "email", + "is_disabled", + "is_system", + }).AddRow( + 10, + "John Doe", + "jon@example.com", + false, + false, + ).AddRow( + 11, + "Jane Doe", + "jane@example.com", + true, + false, + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +func assertModel(t *testing.T, m Model) { + assert.Equal(t, uint(10), m.ID) + assert.Equal(t, "John Doe", m.Name) + assert.Equal(t, "jon@example.com", m.Email) + assert.Equal(t, false, m.IsDisabled) + assert.Equal(t, false, m.IsSystem) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestGetByID() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByID(10) + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestLoadByEmail() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT $4`)). + WithArgs("jon@example.com", false, 0, 1). + WillReturnRows(s.singleRow) + + m, err := GetByEmail("jon@example.com") + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) + assertModel(s.T(), m) +} + +func (s *testsuite) TestIsEnabled() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT $3`)). + WithArgs(10, 0, 1). + WillReturnRows(s.singleRow) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."id" = $1 AND "user"."is_deleted" = $2 ORDER BY "user"."id" LIMIT $3`)). + WithArgs(999, 0, 1). + WillReturnError(goerrors.New("record not found")) + + // user that exists + exists, enabled, err := IsEnabled(10) + require.NoError(s.T(), err) + assert.Equal(s.T(), true, exists) + assert.Equal(s.T(), true, enabled) + // that that doesn't exist + exists, enabled, err = IsEnabled(999) + assert.Equal(s.T(), "record not found", err.Error()) + assert.Equal(s.T(), false, exists) + assert.Equal(s.T(), false, enabled) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSave() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE email = $1 AND is_system = $2 AND "user"."is_deleted" = $3 ORDER BY "user"."id" LIMIT $4`)). + WithArgs("jon@example.com", false, 0, 1). + WillReturnRows(s.singleRow) + + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + "John Doe", + "sarah@example.com", + false, + false, + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) + s.mock.ExpectCommit() + + // New model, as system + m := Model{ + Name: "John Doe", + Email: "JON@example.com", // mixed case on purpose + IsSystem: true, + } + err := m.Save() + assert.Equal(s.T(), errors.ErrSystemUserReadonly.Error(), err.Error()) + + // Remove system and try again. Expect error due to duplicate email + m.IsSystem = false + err = m.Save() + assert.Equal(s.T(), errors.ErrDuplicateEmailUser.Error(), err.Error()) + + // Change email and try again. Expect success + m.Email = "sarah@example.com" + err = m.Save() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestDelete() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`UPDATE "user" SET "is_deleted"=$1 WHERE "user"."id" = $2 AND "user"."is_deleted" = $3`)). + WithArgs(1, 10, 0). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + m := Model{} + err := m.Delete() + assert.Equal(s.T(), "Unable to delete a new object", err.Error()) + + m2 := Model{ + Base: model.Base{ + ID: 10, + }, + Name: "John Doe", + } + err2 := m2.Delete() + require.NoError(s.T(), err2) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGenerateGravatar() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + m := Model{Email: "jon@example.com"} + m.generateGravatar() + assert.Equal(s.T(), "https://www.gravatar.com/avatar/dc36565cc2376197358fa27ed4c47253?d=mm&r=pg&s=128", m.GravatarURL) +} + +func (s *testsuite) TestDeleteAll() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "user" WHERE is_system = $1`)). + WithArgs(false). + WillReturnResult(sqlmock.NewResult(0, 1)) + + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "auth"`)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + err := DeleteAll() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGetCapabilities() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnRows(s.capabilitiesRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(999). + WillReturnRows(sqlmock.NewRows([]string{})) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(1000). + WillReturnError(goerrors.New("some other error")) + + // user that exists + caps, err := GetCapabilities(10) + require.NoError(s.T(), err) + assert.Equal(s.T(), 2, len(caps)) + // user that doesn't exist + caps, err = GetCapabilities(999) + require.NoError(s.T(), err) + assert.Equal(s.T(), 0, len(caps)) + // some other error + caps, err = GetCapabilities(1000) + assert.Equal(s.T(), "some other error", err.Error()) + assert.Equal(s.T(), 0, len(caps)) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestList() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "user" WHERE "user"."name" LIKE $1 AND "user"."is_deleted" = $2`)). + WithArgs("%jon%", 0). + WillReturnRows(s.listCountRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user" WHERE "user"."name" LIKE $1 AND "user"."is_deleted" = $2 ORDER BY name asc LIMIT $3`)). + WithArgs("%jon%", 0, 8). + WillReturnRows(s.listRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnRows(s.capabilitiesRows) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(11). + WillReturnRows(sqlmock.NewRows([]string{})) + + p := model.PageInfo{ + Offset: 0, + Limit: 8, + Sort: []model.Sort{ + { + Field: "name", + Direction: "asc", + }, + }, + } + + f := []model.Filter{ + { + Field: "name", + Modifier: "contains", + Value: []string{"jon"}, + }, + } + + e := []string{"capabilities"} + + resp, err := List(p, f, e) + require.NoError(s.T(), err) + assert.Equal(s.T(), int64(2), resp.Total) + assert.Equal(s.T(), p.Offset, resp.Offset) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Limit, resp.Limit) + assert.Equal(s.T(), p.Sort, resp.Sort) + assert.Equal(s.T(), f, resp.Filter) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSetPermissions() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`INSERT INTO "user_has_capability" ("user_id","capability_name") VALUES ($1,$2),($3,$4)`)). + WithArgs(10, "hosts.view", 10, "hosts.manage"). + WillReturnResult(sqlmock.NewResult(88, 0)) + s.mock.ExpectCommit() + + // Empty model returns error + m := Model{} + err := m.SetPermissions([]string{"hosts.view", "hosts.manage"}) + assert.Equal(s.T(), "Cannot set permissions without first saving the User", err.Error()) + + // Defined user + m.ID = 10 + err = m.SetPermissions([]string{"hosts.view", "hosts.manage"}) + require.NoError(s.T(), err) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSaveCapabilities() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "capability"`)). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow("full-admin"). + AddRow("hosts.view"). + AddRow("hosts.manage")) + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`DELETE FROM "user_has_capability" WHERE user_id = $1`)). + WithArgs(10). + WillReturnResult(sqlmock.NewResult(0, 1)) + s.mock.ExpectCommit() + + s.mock.ExpectBegin() + s.mock. + ExpectExec(regexp.QuoteMeta(`INSERT INTO "user_has_capability" ("user_id","capability_name") VALUES ($1,$2),($3,$4)`)). + WithArgs(10, "hosts.view", 10, "hosts.manage"). + WillReturnResult(sqlmock.NewResult(88, 0)) + s.mock.ExpectCommit() + + // Empty model returns error + m := Model{} + err := m.SaveCapabilities() + assert.Equal(s.T(), "Cannot save capabilities on unsaved user", err.Error()) + + // Empty model returns error + m.ID = 10 + err = m.SaveCapabilities() + assert.Equal(s.T(), "At least 1 capability required for a user", err.Error()) + + // With some caps + m.Capabilities = []string{"hosts.view", "hosts.manage"} + err = m.SaveCapabilities() + require.NoError(s.T(), err) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestSaveCapabilitiesInvalid() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "capability"`)). + WillReturnRows(sqlmock.NewRows([]string{"name"}). + AddRow("full-admin"). + AddRow("hosts.view"). + AddRow("hosts.manage")) + + // Empty model returns error + m := Model{ + Base: model.Base{ + ID: 10, + }, + Capabilities: []string{"doesnotexist", "hosts.manage"}, + } + err := m.SaveCapabilities() + assert.Equal(s.T(), "Capability `doesnotexist` is not valid", err.Error()) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go new file mode 100644 index 000000000..1002d1855 --- /dev/null +++ b/backend/internal/entity/user/methods.go @@ -0,0 +1,142 @@ +package user + +import ( + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/entity/auth" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a user by ID +func GetByID(id uint) (Model, error) { + var m Model + err := m.LoadByID(id) + m.generateGravatar() + 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 +} + +// IsEnabled is used by middleware to ensure the user is still enabled +// returns (userExist, isEnabled, error) +func IsEnabled(userID uint) (bool, bool, error) { + var user Model + db := database.GetDB() + if result := db.First(&user, userID); result.Error != nil { + return false, false, result.Error + } + return true, !user.IsDisabled, nil +} + +// List will return a list of users +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (entity.ListResponse, error) { + var result entity.ListResponse + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + dbo := entity.ListQueryBuilder(&pageInfo, filters, entity.GetFilterMap(Model{}, true)) + + // Get count of items in this search + var totalRows int64 + if res := dbo.Model(&Model{}).Count(&totalRows); res.Error != nil { + return result, res.Error + } + + // Get rows + dbo = entity.AddOffsetLimitToList(dbo, &pageInfo) + dbo = entity.AddOrderToList(dbo, pageInfo.Sort, defaultSort) + items := make([]Model, 0) + if res := dbo.Find(&items); res.Error != nil { + return result, res.Error + } + + for idx := range items { + items[idx].generateGravatar() + } + + if expand != nil { + for idx := range items { + expandErr := items[idx].Expand(expand) + if expandErr != nil { + logger.Error("UsersExpansionError", expandErr) + } + } + } + + result = entity.ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.GetSort(defaultSort), + Filter: filters, + } + + return result, nil +} + +// DeleteAll will do just that, and should only be used for testing purposes. +func DeleteAll() error { + db := database.GetDB() + if result := db.Exec( + fmt.Sprintf(`DELETE FROM %s WHERE is_system = ?`, database.QuoteTableName("user")), + false, + ); result.Error != nil { + return result.Error + } + + if result := db.Exec( + fmt.Sprintf(`DELETE FROM %s`, database.QuoteTableName("auth")), + ); result.Error != nil { + return result.Error + } + + return nil +} + +// GetCapabilities gets capabilities for a user +func GetCapabilities(userID uint) ([]string, error) { + capabilities := make([]string, 0) + var hasCapabilities []HasCapabilityModel + db := database.GetDB() + if result := db.Where("user_id = ?", userID).Find(&hasCapabilities); result.Error != nil { + return nil, result.Error + } + for _, obj := range hasCapabilities { + capabilities = append(capabilities, obj.CapabilityName) + } + return capabilities, nil +} + +// CreateFromLDAPUser will create a user from an LDAP user object +func CreateFromLDAPUser(ldapUser *auth.LDAPUser) (Model, error) { + user := Model{ + Email: ldapUser.Email, + Name: ldapUser.Name, + } + err := user.Save() + user.generateGravatar() + return user, err +} + +// CreateFromOAuthUser will create a user from an OAuth user object +func CreateFromOAuthUser(ou *auth.OAuthUser) (Model, error) { + user := Model{ + Email: ou.GetEmail(), + Name: ou.GetName(), + } + err := user.Save() + user.generateGravatar() + return user, err +} diff --git a/backend/internal/entity/user/model.go b/backend/internal/entity/user/model.go new file mode 100644 index 000000000..cb49457da --- /dev/null +++ b/backend/internal/entity/user/model.go @@ -0,0 +1,172 @@ +package user + +import ( + "strings" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/entity/auth" + "npm/internal/errors" + "npm/internal/model" + "npm/internal/util" + + "github.com/drexedam/gravatar" + "github.com/rotisserie/eris" +) + +// Model is the model +type Model struct { + model.Base + Name string `json:"name" gorm:"column:name" filter:"name,string"` + Email string `json:"email" gorm:"column:email" filter:"email,email"` + IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` + IsSystem bool `json:"is_system,omitempty" gorm:"column:is_system" filter:"is_system,boolean"` + // Other + GravatarURL string `json:"gravatar_url" gorm:"-"` + // Expansions + Auth *auth.Model `json:"auth,omitempty" gorm:"-"` + Capabilities []string `json:"capabilities,omitempty" gorm:"-"` +} + +// TableName overrides the table name used by gorm +func (Model) TableName() string { + return "user" +} + +// HasCapabilityModel is the model +type HasCapabilityModel struct { + UserID uint `json:"user_id" gorm:"column:user_id"` + CapabilityName string `json:"name" gorm:"column:capability_name"` +} + +// TableName overrides the table name used by gorm +func (HasCapabilityModel) TableName() string { + return "user_has_capability" +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id uint) error { + db := database.GetDB() + result := db.First(&m, id) + return result.Error +} + +// LoadByEmail will load from an Email +func (m *Model) LoadByEmail(email string) error { + db := database.GetDB() + result := db. + Where("email = ?", strings.TrimSpace(strings.ToLower(email))). + Where("is_system = ?", false). + First(&m) + return result.Error +} + +// Save will save this model to the DB +func (m *Model) Save() error { + if m.IsSystem { + return errors.ErrSystemUserReadonly + } + + // Ensure email is nice + m.Email = strings.TrimSpace(strings.ToLower(m.Email)) + + // Check if an existing user with this email exists + if m2, err := GetByEmail(m.Email); err == nil && m.ID != m2.ID { + return errors.ErrDuplicateEmailUser + } + + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// Delete will mark a user as deleted +func (m *Model) Delete() error { + if m.ID == 0 { + // Can't delete a new object + return eris.New("Unable to delete a new object") + } + db := database.GetDB() + result := db.Delete(m) + return result.Error +} + +// SetPermissions will wipe out any existing permissions and add new ones for this user +func (m *Model) SetPermissions(permissions []string) error { + if m.ID == 0 { + return eris.Errorf("Cannot set permissions without first saving the User") + } + + db := database.GetDB() + // Wipe out previous permissions + if result := db.Where("user_id = ?", m.ID).Delete(&HasCapabilityModel{}); result.Error != nil { + return result.Error + } + + if len(permissions) > 0 { + // Add new permissions + objs := []*HasCapabilityModel{} + for _, permission := range permissions { + objs = append(objs, &HasCapabilityModel{UserID: m.ID, CapabilityName: permission}) + } + if result := db.Create(objs); result.Error != nil { + return result.Error + } + } + + return nil +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + if util.SliceContainsItem(items, "capabilities") && m.ID > 0 { + m.Capabilities, err = GetCapabilities(m.ID) + } + + return err +} + +func (m *Model) generateGravatar() { + m.GravatarURL = gravatar.New(m.Email). + Size(128). + Default(gravatar.MysteryMan). + Rating(gravatar.Pg). + AvatarURL() +} + +// SaveCapabilities will save the capabilities of the user. +func (m *Model) SaveCapabilities() error { + // m.Capabilities + if m.ID == 0 { + return eris.Errorf("Cannot save capabilities on unsaved user") + } + + // there must be at least 1 capability + if len(m.Capabilities) == 0 { + return eris.New("At least 1 capability required for a user") + } + + db := database.GetDB() + // Get a full list of capabilities + var capabilities []entity.Capability + if result := db.Find(&capabilities); result.Error != nil { + return result.Error + } + + // Check that the capabilities defined exist in the db + for _, cap := range m.Capabilities { + found := false + for _, a := range capabilities { + if a.Name == cap { + found = true + } + } + if !found { + return eris.Errorf("Capability `%s` is not valid", cap) + } + } + + return m.SetPermissions(m.Capabilities) +} diff --git a/backend/internal/errors/errors.go b/backend/internal/errors/errors.go new file mode 100644 index 000000000..16be50d24 --- /dev/null +++ b/backend/internal/errors/errors.go @@ -0,0 +1,20 @@ +package errors + +import ( + "github.com/rotisserie/eris" +) + +// All error messages used by the service package to report +// problems back to calling clients +var ( + ErrDatabaseUnavailable = eris.New("database-unavailable") + ErrDuplicateEmailUser = eris.New("email-already-exists") + ErrInvalidLogin = eris.New("invalid-login-credentials") + ErrInvalidAuthType = eris.New("invalid-auth-type") + ErrUserDisabled = eris.New("user-disabled") + ErrSystemUserReadonly = eris.New("cannot-save-system-users") + ErrValidationFailed = eris.New("request-failed-validation") + ErrCurrentPasswordInvalid = eris.New("current-password-invalid") + ErrCABundleDoesNotExist = eris.New("ca-bundle-does-not-exist") + ErrProviderNotFound = eris.New("provider_not_found") +) diff --git a/backend/internal/host.js b/backend/internal/host.js deleted file mode 100644 index 58e1d09a4..000000000 --- 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 40e63ea40..000000000 --- a/backend/internal/ip_ranges.js +++ /dev/null @@ -1,150 +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 regIpV4 = /^(\d+\.?){4}\/\d+/; -const regIpV6 = /^(([\da-fA-F]+)?:)+\/\d+/; - -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').filter((line) => regIpV4.test(line)); - ip_ranges = [... ip_ranges, ... items]; - }) - .then(() => { - return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL); - }) - .then((cloudfare_data) => { - let items = cloudfare_data.split('\n').filter((line) => regIpV6.test(line)); - 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/jobqueue/main.go b/backend/internal/jobqueue/main.go new file mode 100644 index 000000000..22793186e --- /dev/null +++ b/backend/internal/jobqueue/main.go @@ -0,0 +1,49 @@ +package jobqueue + +import ( + "context" + + "github.com/rotisserie/eris" +) + +var ( + ctx context.Context + cancel context.CancelFunc + worker *Worker +) + +// Start will intantiate the queue and start doing work +func Start() { + ctx, cancel = context.WithCancel(context.Background()) + q := &Queue{ + jobs: make(chan Job, 50), + ctx: ctx, + cancel: cancel, + } + + // Defines a queue worker, which will execute our queue. + worker = newWorker(q) + + // Execute jobs in queue. + go worker.doWork() +} + +// Shutdown will gracefully stop the queue +func Shutdown() error { + if cancel == nil { + return eris.New("Unable to shutdown, jobqueue has not been started") + } + cancel() + worker = nil + cancel = nil + return nil +} + +// AddJob adds a job to the queue for processing +func AddJob(j Job) error { + if worker == nil { + return eris.New("Unable to add job, jobqueue has not been started") + } + worker.Queue.AddJob(j) + return nil +} diff --git a/backend/internal/jobqueue/main_test.go b/backend/internal/jobqueue/main_test.go new file mode 100644 index 000000000..bebdc7d1c --- /dev/null +++ b/backend/internal/jobqueue/main_test.go @@ -0,0 +1,67 @@ +package jobqueue + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/rotisserie/eris" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockJob struct { + done chan bool +} + +func (m *MockJob) Execute() { + time.Sleep(1 * time.Second) + m.done <- true +} + +func TestStart(t *testing.T) { + Start() + assert.NotNil(t, ctx, "Context should not be nil after Start") + assert.NotNil(t, cancel, "Cancel function should not be nil after Start") + assert.NotNil(t, worker, "Worker should not be nil after Start") + Shutdown() +} + +func TestShutdown(t *testing.T) { + Start() + err := Shutdown() + require.Nil(t, err, "Shutdown should not return an error when jobqueue is started") + + // nolint: gosimple + select { + case <-ctx.Done(): + switch ctx.Err() { + case context.DeadlineExceeded: + fmt.Println("context timeout exceeded") + case context.Canceled: + fmt.Println("context cancelled by force. whole process is complete") + default: + require.Nil(t, ctx.Err(), "Context done state has unexpected value") + } + } + + require.Nil(t, cancel, "Cancel function should be nil after Shutdown") + require.Nil(t, worker, "Worker should be nil after Shutdown") + + err = Shutdown() + require.NotNil(t, err, "Shutdown should return an error when jobqueue is not started") + require.Equal(t, eris.New("Unable to shutdown, jobqueue has not been started").Error(), err.Error()) +} + +func TestAddJobWithoutStart(t *testing.T) { + mockJob := Job{ + Name: "mockJob", + Action: func() error { + return nil + }, + } + err := AddJob(mockJob) + assert.NotNil(t, err, "AddJob should return an error when jobqueue is not started") + assert.Equal(t, eris.New("Unable to add job, jobqueue has not been started").Error(), err.Error()) +} diff --git a/backend/internal/jobqueue/models.go b/backend/internal/jobqueue/models.go new file mode 100644 index 000000000..410a788fb --- /dev/null +++ b/backend/internal/jobqueue/models.go @@ -0,0 +1,52 @@ +package jobqueue + +import ( + "context" + "sync" +) + +// Queue holds name, list of jobs and context with cancel. +type Queue struct { + jobs chan Job + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex +} + +// Job - holds logic to perform some operations during queue execution. +type Job struct { + Name string + Action func() error // A function that should be executed when the job is running. +} + +// AddJobs adds jobs to the queue and cancels channel. +func (q *Queue) AddJobs(jobs []Job) { + var wg sync.WaitGroup + wg.Add(len(jobs)) + + for _, job := range jobs { + // Goroutine which adds job to the queue. + go func(job Job) { + q.AddJob(job) + wg.Done() + }(job) + } + + go func() { + wg.Wait() + // Cancel queue channel, when all goroutines were done. + q.cancel() + }() +} + +// AddJob sends job to the channel. +func (q *Queue) AddJob(job Job) { + q.mu.Lock() + defer q.mu.Unlock() + q.jobs <- job +} + +// Run performs job execution. +func (j *Job) Run() error { + return j.Action() +} diff --git a/backend/internal/jobqueue/worker.go b/backend/internal/jobqueue/worker.go new file mode 100644 index 000000000..f431b82a6 --- /dev/null +++ b/backend/internal/jobqueue/worker.go @@ -0,0 +1,37 @@ +package jobqueue + +import ( + "fmt" + + "npm/internal/logger" +) + +// Worker responsible for queue serving. +type Worker struct { + Queue *Queue +} + +func newWorker(queue *Queue) *Worker { + return &Worker{ + Queue: queue, + } +} + +// doWork processes jobs from the queue (jobs channel). +func (w *Worker) doWork() bool { + for { + select { + // if context was canceled. + case <-w.Queue.ctx.Done(): + logger.Info("JobQueue worker graceful shutdown") + return true + // if job received. + case job := <-w.Queue.jobs: + err := job.Run() + if err != nil { + logger.Error(fmt.Sprintf("%sError", job.Name), err) + continue + } + } + } +} diff --git a/backend/internal/jwt/jwt.go b/backend/internal/jwt/jwt.go new file mode 100644 index 000000000..acc365fa0 --- /dev/null +++ b/backend/internal/jwt/jwt.go @@ -0,0 +1,65 @@ +package jwt + +import ( + "time" + + "npm/internal/entity/user" + "npm/internal/logger" + + "github.com/dgrijalva/jwt-go" + "github.com/rotisserie/eris" +) + +// UserJWTClaims is the structure of a JWT for a User +type UserJWTClaims struct { + UserID uint `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, forSSE bool) (GeneratedResponse, error) { + var response GeneratedResponse + + key, _ := GetPrivateKey() + expires := time.Now().AddDate(0, 0, 1) // 1 day + issuer := "api" + + if forSSE { + issuer = "sse" + } + + // Create the Claims + claims := UserJWTClaims{ + userObj.ID, + []string{"user"}, + jwt.StandardClaims{ + IssuedAt: time.Now().Unix(), + ExpiresAt: expires.Unix(), + Issuer: issuer, + }, + } + + // 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", eris.Wrapf(err, "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 000000000..b082c4a4e --- /dev/null +++ b/backend/internal/jwt/keys.go @@ -0,0 +1,135 @@ +package jwt + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/pem" + + "npm/internal/logger" + + "github.com/rotisserie/eris" +) + +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 currentKeys.PrivateKey == "" { + return blankKey, eris.New("Could not get Private Key from configuration") + } + + var err error + privateKey, err = LoadPemPrivateKey(currentKeys.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 currentKeys.PublicKey == "" { + return blankKey, eris.New("Could not get Public Key from configuration") + } + + var err error + publicKey, err = LoadPemPublicKey(currentKeys.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 +} + +func generateKeys() (KeysModel, error) { + m := KeysModel{} + reader := rand.Reader + bitSize := 4096 + + key, err := rsa.GenerateKey(reader, bitSize) + if err != nil { + return m, err + } + + privateKey := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + privateKeyBuffer := new(bytes.Buffer) + err = pem.Encode(privateKeyBuffer, privateKey) + if err != nil { + return m, err + } + + asn1Bytes, err := asn1.Marshal(key.PublicKey) + if err != nil { + return m, err + } + + publicKey := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: asn1Bytes, + } + + publicKeyBuffer := new(bytes.Buffer) + err = pem.Encode(publicKeyBuffer, publicKey) + if err != nil { + return m, err + } + + m.PublicKey = publicKeyBuffer.String() + m.PrivateKey = privateKeyBuffer.String() + + logger.Info("Generated new RSA keys") + + return m, nil +} diff --git a/backend/internal/jwt/keys_db.go b/backend/internal/jwt/keys_db.go new file mode 100644 index 000000000..e3ecd55dc --- /dev/null +++ b/backend/internal/jwt/keys_db.go @@ -0,0 +1,52 @@ +package jwt + +import ( + "npm/internal/database" + "npm/internal/model" +) + +var currentKeys KeysModel + +// KeysModel is the model +type KeysModel struct { + model.Base + PublicKey string `gorm:"column:public_key"` + PrivateKey string `gorm:"column:private_key"` +} + +// TableName overrides the table name used by gorm +func (KeysModel) TableName() string { + return "jwt_keys" +} + +// LoadLatest will load the latest keys +func (m *KeysModel) LoadLatest() error { + db := database.GetDB() + result := db.Order("created_at DESC").First(&m) + return result.Error +} + +// Save will save this model to the DB +func (m *KeysModel) Save() error { + db := database.GetDB() + result := db.Save(m) + return result.Error +} + +// LoadKeys will load from the database, or generate and save new ones +func LoadKeys() error { + // Try to find in db + if err := currentKeys.LoadLatest(); err != nil { + // Keys probably don't exist, so we need to generate some + if currentKeys, err = generateKeys(); err != nil { + return err + } + + // and save them + if err = currentKeys.Save(); err != nil { + return err + } + } + + return nil +} diff --git a/backend/internal/jwt/suite_test.go b/backend/internal/jwt/suite_test.go new file mode 100644 index 000000000..b17f5d677 --- /dev/null +++ b/backend/internal/jwt/suite_test.go @@ -0,0 +1,255 @@ +package jwt + +import ( + "regexp" + "testing" + + "npm/internal/entity/user" + "npm/internal/model" + "npm/internal/test" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/goleak" + "gorm.io/gorm" +) + +// +------------+ +// | Setup | +// +------------+ + +type testsuite struct { + suite.Suite + mock sqlmock.Sqlmock + keysRow *sqlmock.Rows + privateKeyString string + publicKeyString string +} + +// SetupTest is executed before each test +func (s *testsuite) SetupTest() { + var err error + s.mock, err = test.Setup() + require.NoError(s.T(), err) + test.InitConfig(s.T()) + + s.publicKeyString = `-----BEGIN PUBLIC KEY----- +MIICCgKCAgEAmjPv7Bnb02rdcMqPIK6EMt7hIYzobgmIwoEtNVP6IaVzPdTo0l5V +prxVH9J2oeJzZPmUjgsru+1db/RqOT4QYma7FGjoVi/AZvGGbiJENOK87K0d3byM +CZ1bridvVOWKU92EvK+uTBfG9wtEpyS4pTC6mt6jKnKJrlRA7pKbHB7jmgBDU+6C +CniYIkXpmHKQu1Q4Mpa5oYzgNMiAnxRmps8BwkVNwzMc7mwdhSn+M+qkVJJO4Q5A +BgHoieR6y5+P2oieX4Z8HwnzxKS79FmTo0JhSVadPxYTRKWLAcVgM7lo/d+KQy7o +v+jFVYNiaeSdAPgfEoYdiJA7NZC4xEVsQMeZLGZp1WpcwJ7DInn0Z4ILsuFIeQGv +nmaq+yFRgJcQT3zANlitqwdK4FWd0DN1PH17YG3oIGvxHCFEClZqeLPte1lB3T3w +xyrw00OfSKBwZ9gAxemxQRu/8/9EnLOM4FlanK8/S4GIPgj05LvE52ZLotXqQU77 +PIXaGwufKkwOx0oHmy82wZpc4A18Po6UMcsnyfR+ykKHacuRKCHnJjCEszKR8t/i +BqL4+sCPZfE/6FSjZi1tHGTybMGVTEL/HnJEHOZqvWbKwl6xLln38UWi7YG/FFs/ +ymcpxqEnMWQCs8ZmA1q6nPkzHkjYGmr5SyC6jaFaKJN1SgCsaP2sF80CAwEAAQ== +-----END PUBLIC KEY-----` + + s.privateKeyString = `-----BEGIN PRIVATE KEY----- +MIIJKQIBAAKCAgEAmjPv7Bnb02rdcMqPIK6EMt7hIYzobgmIwoEtNVP6IaVzPdTo +0l5VprxVH9J2oeJzZPmUjgsru+1db/RqOT4QYma7FGjoVi/AZvGGbiJENOK87K0d +3byMCZ1bridvVOWKU92EvK+uTBfG9wtEpyS4pTC6mt6jKnKJrlRA7pKbHB7jmgBD +U+6CCniYIkXpmHKQu1Q4Mpa5oYzgNMiAnxRmps8BwkVNwzMc7mwdhSn+M+qkVJJO +4Q5ABgHoieR6y5+P2oieX4Z8HwnzxKS79FmTo0JhSVadPxYTRKWLAcVgM7lo/d+K +Qy7ov+jFVYNiaeSdAPgfEoYdiJA7NZC4xEVsQMeZLGZp1WpcwJ7DInn0Z4ILsuFI +eQGvnmaq+yFRgJcQT3zANlitqwdK4FWd0DN1PH17YG3oIGvxHCFEClZqeLPte1lB +3T3wxyrw00OfSKBwZ9gAxemxQRu/8/9EnLOM4FlanK8/S4GIPgj05LvE52ZLotXq +QU77PIXaGwufKkwOx0oHmy82wZpc4A18Po6UMcsnyfR+ykKHacuRKCHnJjCEszKR +8t/iBqL4+sCPZfE/6FSjZi1tHGTybMGVTEL/HnJEHOZqvWbKwl6xLln38UWi7YG/ +FFs/ymcpxqEnMWQCs8ZmA1q6nPkzHkjYGmr5SyC6jaFaKJN1SgCsaP2sF80CAwEA +AQKCAgEAmM9ZNd6WQleHZAvHdHqc1RCbhzTs7IaUOTPrygoTOR6NKjwAEOCc/mNp +8+QL3fbbpbfSqESXrV7XFmfekCVZ9TmasOoZO7eMcjdsoV1hvArpb51KmH8NQ0Xm +IZpAsJ/byaoerSFnl06ExDItcXlpZYH5mhmBFkJ1AAXMZt9vyJkvsWALWHRl99xz +3prrl0AI/yrBmhhVkqtZT9VV6M89vpYrRwqIuiS/yeHoCxuHJomjGY/3jP0jIxDn +EScTLRBNbSGv2DgcbmHdaQRaohXWwZW5dQTZRTgqFf/61eFzqS5WxiatDFDDI9KX +I1vUvd1oXRqFKEUxpTBRDI8DGrU1RR7140FsevGo8Z4xctpPml8vxv0D/77p0PHF +9wCW6NLQeR/v4E8tEzrH3tpx/RtS8VqpMMbTbzvAWqhcWuEAjix4W0X6poR9QEUl +p0Xm7Ut726QMN8dxbp91C1kkLO5m0mll3dlZrqhJ0FcB3eyNNO7GOfteQcOiyYfM +HayjA7yVPQTVy92lo77HQPXakydVCZVfUosN7zMf+7nKVh/SYzaTMF9OxcyLyNEE ++Dvjy3KbCCWFxxkqwICackjIIESFpFmmT5ZY7Hx7EImk+TEh7EpTv55/ZNlodDwh +qE1zlfOaUnKzmsp5ET8WpXUUv4wRLQxtIrtTGvvNPHzRRQZWn0ECggEBAMkqQ8gR +Pv3XvBNVqHtluDe5pmNg2K5/VCWDgndXSIyxM5Pr4Nr3z5mfnQ01Z/T1smlk+dVy +oD8dsj1XB23e36IPSFtYlNjv58eo0Fcv2XlGYK9a/10IHZ/jNVRIgUdH/KTuZ154 +JRZ5LR/+gjmBqkWmG+bODlBXnp96BUw6rwaYMDecCnOKoKOhlkvGg3aJx/l8sptx +TVhSwPCRVFff53rMsad60+Vjv9DohhWlotuujQGH1Cvw7aacFm20888IOBGGEIEZ +u3pvr3/8L0LZDXdoR1MMcdopDek+fU8fS5M5STv2/rQauGMtCJPu615uup1IBOaV +soYraIlqh+wT9t0CggEBAMQ8jCOJVZe11voX5F5fbYKr/fWP4P2QDXk/DlX0VQmX +AgSljT5ePcDOFwz6Xaa7HvMq22IOssl7e3vTrS5HJd7k/e4TzhBz6TSMMuuvgN2l +JkWRr1dsBVJXOJxQt65t34GCZEf8UikSQaWnyyRmats8H9iG0wQr1jXJxULUOd7h +y/d1gfwujKjws1NRWFYkl1ZXeWUGpuEGR+96CgMMXoEoQr8fGEe19QBflBOQVEhf +KbH6qmvoBARYAUpU5nszst2HeChv84a238gOMSuhHwi2oHM3zpkU0m36mDoVulc7 +heqb/v584AZWesJ5ShouVHc4HreRPHx8Xrxmv1ce/bECggEAJi5udQ/I6/dBjE3q +z5kL8Q+8pAoitmQWfZRLdAlODN4pUv8nS4hTj+36qiIj3BuyREzVGo1KGxCw3vGg +yFrQCXtrGWNjxRUr4fqJqLK9TUZtXXshEvBSZyGB4sBsQTJJoqhZWFXnfC99wB/X +acDRp6ySiSk9EETBJ7XKQaC1zcOfCz8DwNBkEwq9cx53n00hdpoTcGt96bCzTDXZ +U2B9GBK3+XjXtSdMpgMsR/mLQrULsGmufLSa9s+TdjktOXNu6OyQP2C589A0+E7O +TZrS8oIJX5ryFR1LtaSVtinTd1sdKlOEHn0f2DsY8LMdW2wa4XVk8LsjClI84jAl +IkrbxQKCAQANRIa5FFz8H+hECn9/PfZ6gkRuaObuXeH7U58VgqqJNnOFeuf80oRc +V9LJJthUIIysJjak/5do9fdYXOx1l4vg8RyWDzK8fAnFasE6nCgbVEItK/dt8ri9 +Y3ZJY0+39GfLKtS65T1s13YmzBx4/o+0+PCyRBNaUdhu1JCIvy6Wei+/MGu0cDVE +atnFBVfyoxC0Xr+va+62giU09MxefmSZWO6CW4jZuFyzRMMPO4/nQL/h76+8EfjL +jmOv8eOPauRqA/HE0iTl89FXhlYevAsMHMTmZVyLjxPXKb1HGBb8NOMOBLQN4sWG +yCwOoAK5mG5PjTTOdnxfck05cbz4F/lRAoIBAQCDG6yy1abapV+0Yfe58g3KVkUn +4pNbb30CERQvwReEgF/sI0Kr3dQ4RvF4NQfRpODoakvAQfbrhg29juzT7O0Lk/kP +tPd0xat/r7pnq1kn4rQmgzIWzPPC72BoAjDEkdyB9u9a1RSqBHcQ0st81sVSNeoZ +OzTJqfuKN8R71VA/8ujMlnWEdfPF+SBc/01CChhLfuWiiATPhqG7wypBt5TBAMpa +58rkFlrsTz0qWt+jyLQqJObPk/aVwXQT9QpihEp1IrDRUnU3gP0fjUTkXSVdNRvp +CC3OVnreGq4pnTKFlElta1kgenXb+zbjwVwZntxmgP1z/Q9m/yToyLehDfk+ +-----END PRIVATE KEY-----` + + // These rows need to be intantiated for each test as they are + // read in the db object, and their row position is not resettable + // between tests. + s.keysRow = sqlmock.NewRows([]string{ + "public_key", + "private_key", + }).AddRow( + s.publicKeyString, + s.privateKeyString, + ) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(testsuite)) +} + +// +------------+ +// | Tests | +// +------------+ + +func (s *testsuite) TestLoadKeys() { + s.T().Skip("Skipping as it's not working") + + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Required for clean test runs + currentKeys = KeysModel{} + + // first query, no rows + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "jwt_keys" WHERE "jwt_keys"."is_deleted" = $1`)). + WithArgs(0). + WillReturnError(gorm.ErrRecordNotFound) + + // insert row + s.mock.ExpectBegin() + s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "jwt_keys" ("created_at","updated_at","is_deleted","public_key","private_key") VALUES ($1,$2,$3,$4,$5) RETURNING "id"`)). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + 0, + sqlmock.AnyArg(), + sqlmock.AnyArg(), + ). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("10")) + s.mock.ExpectCommit() + + // last query, load existing row + s.mock. + ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "jwt_keys" WHERE "jwt_keys"."is_deleted" = $1`)). + WithArgs(0). + WillReturnRows(s.keysRow) + + // Load and create first + err := LoadKeys() + require.NoError(s.T(), err) + + // Load something we just created + k := KeysModel{} + err = k.LoadLatest() + require.NoError(s.T(), err) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGetPrivateKey() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Required for clean test runs + privateKey = nil + currentKeys = KeysModel{} + + // First call, expect error as currentKeys isn't loaded + _, err := GetPrivateKey() + assert.Equal(s.T(), "Could not get Private Key from configuration", err.Error()) + + // Set currentKeys and try again + currentKeys = KeysModel{ + Base: model.Base{ + ID: 10, + }, + PrivateKey: s.privateKeyString, + PublicKey: s.publicKeyString, + } + + // Get after currentKeys is set + _, err = GetPrivateKey() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGetPublicKey() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Required for clean test runs + publicKey = nil + currentKeys = KeysModel{} + + // First call, expect error as currentKeys isn't loaded + _, err := GetPublicKey() + assert.Equal(s.T(), "Could not get Public Key from configuration", err.Error()) + + // Set currentKeys and try again + currentKeys = KeysModel{ + Base: model.Base{ + ID: 10, + }, + PrivateKey: s.privateKeyString, + PublicKey: s.publicKeyString, + } + + // Get after currentKeys is set + _, err = GetPublicKey() + require.NoError(s.T(), err) + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} + +func (s *testsuite) TestGenerate() { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(s.T(), goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + currentKeys = KeysModel{ + Base: model.Base{ + ID: 10, + }, + PrivateKey: s.privateKeyString, + PublicKey: s.publicKeyString, + } + + usr := user.Model{ + Base: model.Base{ + ID: 10, + }, + } + + // test 1, user key + r, err := Generate(&usr, false) + require.NoError(s.T(), err) + assert.Greater(s.T(), len(r.Token), 20) + + // test 2, sse key + r, err = Generate(&usr, true) + require.NoError(s.T(), err) + assert.Greater(s.T(), len(r.Token), 20) + + require.NoError(s.T(), s.mock.ExpectationsWereMet()) +} diff --git a/backend/internal/logger/config.go b/backend/internal/logger/config.go new file mode 100644 index 000000000..abbfbf7a2 --- /dev/null +++ b/backend/internal/logger/config.go @@ -0,0 +1,37 @@ +package logger + +// 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 +} + +// Interface for a logger +type Interface interface { + GetLogLevel() Level + Debug(format string, args ...any) + Info(format string, args ...any) + Warn(format string, args ...any) + Error(errorClass string, err error, args ...any) + Errorf(errorClass, format string, err error, args ...any) +} + +// 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 000000000..56be4b71c --- /dev/null +++ b/backend/internal/logger/logger.go @@ -0,0 +1,223 @@ +package logger + +import ( + "encoding/json" + "fmt" + stdlog "log" + "os" + "runtime/debug" + "sync" + "time" + + "github.com/fatih/color" + "github.com/rotisserie/eris" +) + +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 ...any) { + 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 ...any) { + 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 ...any) { + 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) +} + +// Get returns the logger +func Get() *Logger { + return logger +} + +// 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 eris.Errorf("a non nil Config is mandatory") + } + + if err := c.LogThreshold.validate(); err != nil { + return err + } + + l.LogThreshold = c.LogThreshold + l.Formatter = c.Formatter + + 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 eris.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 ...any) { + 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 = []any{} + } + } + + 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 ...any) { + 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 ...any) { + 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 ...any) { + 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) +} diff --git a/backend/internal/logger/logger_test.go b/backend/internal/logger/logger_test.go new file mode 100644 index 000000000..9fb765292 --- /dev/null +++ b/backend/internal/logger/logger_test.go @@ -0,0 +1,204 @@ +package logger + +import ( + "bytes" + "io" + "log" + "os" + "testing" + + "github.com/rotisserie/eris" + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetLogLevel(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + assert.Equal(t, InfoLevel, GetLogLevel()) +} + +func TestThreshold(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + 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", eris.New("this should display")) + assert.NotEmpty(t, buf.String()) +} + +func TestDebug(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: DebugLevel, + Formatter: "json", + })) + + Debug("This is a %s message", "test") + assert.Contains(t, buf.String(), "DEBUG") + assert.Contains(t, buf.String(), "This is a test message") + Get() +} + +func TestInfo(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + 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) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + 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) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + log.SetOutput(os.Stderr) + }() + + assert.NoError(t, Configure(&Config{ + LogThreshold: ErrorLevel, + })) + + Error("TestErrorClass", eris.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) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type args struct { + c *Config + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "configure", + args: args{ + &Config{ + LogThreshold: InfoLevel, + }, + }, + wantErr: false, + }, + { + name: "configure json", + args: args{ + &Config{ + LogThreshold: InfoLevel, + Formatter: "json", + }, + }, + wantErr: false, + }, + { + name: "invalid log level", + args: args{ + &Config{}, + }, + wantErr: true, + }, + { + name: "invalid config struct", + args: args{ + nil, + }, + 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(io.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(io.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/cloudfrontranges.go b/backend/internal/model/cloudfrontranges.go new file mode 100644 index 000000000..bdb087742 --- /dev/null +++ b/backend/internal/model/cloudfrontranges.go @@ -0,0 +1,17 @@ +package model + +// CloudfrontIPRangePrefix is used within config for cloudfront +type CloudfrontIPRangeV4Prefix struct { + Value string `json:"ip_prefix"` +} + +// CloudfrontIPRangeV6Prefix is used within config for cloudfront +type CloudfrontIPRangeV6Prefix struct { + Value string `json:"ipv6_prefix"` +} + +// CloudfrontIPRanges is the main config for cloudfront +type CloudfrontIPRanges struct { + IPV4Prefixes []CloudfrontIPRangeV4Prefix `json:"prefixes"` + IPV6Prefixes []CloudfrontIPRangeV6Prefix `json:"ipv6_prefixes"` +} diff --git a/backend/internal/model/filter.go b/backend/internal/model/filter.go new file mode 100644 index 000000000..2cdf560c6 --- /dev/null +++ b/backend/internal/model/filter.go @@ -0,0 +1,16 @@ +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"` +} + +// FilterMapValue is the structure of a filter map value +type FilterMapValue struct { + Type string + Field string + Schema string + Model string +} diff --git a/backend/internal/model/model_base.go b/backend/internal/model/model_base.go new file mode 100644 index 000000000..8465f95e9 --- /dev/null +++ b/backend/internal/model/model_base.go @@ -0,0 +1,13 @@ +package model + +import ( + "gorm.io/plugin/soft_delete" +) + +// Base include common fields for db control +type Base struct { + ID uint `json:"id" gorm:"column:id;primaryKey" filter:"id,integer"` + CreatedAt int64 `json:"created_at" gorm:"<-:create;autoCreateTime:milli;column:created_at" filter:"created_at,date"` + UpdatedAt int64 `json:"updated_at" gorm:"<-;autoUpdateTime:milli;column:updated_at" filter:"updated_at,date"` + DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:is_deleted;softDelete:flag"` +} diff --git a/backend/internal/model/pageinfo.go b/backend/internal/model/pageinfo.go new file mode 100644 index 000000000..ad77a6812 --- /dev/null +++ b/backend/internal/model/pageinfo.go @@ -0,0 +1,24 @@ +package model + +// PageInfo is the model used by Api Handlers and passed on to other parts +// of the application +type PageInfo struct { + Sort []Sort `json:"sort"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Expand []string `json:"expand"` +} + +// Sort holds the sorting data +type Sort struct { + Field string `json:"field"` + Direction string `json:"direction"` +} + +// GetSort is the sort array +func (p *PageInfo) GetSort(def Sort) []Sort { + if p.Sort == nil { + return []Sort{def} + } + return p.Sort +} diff --git a/backend/internal/model/pageinfo_test.go b/backend/internal/model/pageinfo_test.go new file mode 100644 index 000000000..e3d20e83e --- /dev/null +++ b/backend/internal/model/pageinfo_test.go @@ -0,0 +1,31 @@ +package model + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestPageInfoGetSort(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("testing.tRunner.func1")) + + t.Parallel() + pi := PageInfo{} + def := Sort{ + Field: "name", + Direction: "asc", + } + defined := Sort{ + Field: "email", + Direction: "desc", + } + // default + sort := pi.GetSort(def) + assert.Equal(t, sort, []Sort{def}) + // defined + pi.Sort = []Sort{defined} + sort = pi.GetSort(def) + assert.Equal(t, sort, []Sort{defined}) +} diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js deleted file mode 100644 index 52bdd66dc..000000000 --- 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/nginx/control.go b/backend/internal/nginx/control.go new file mode 100644 index 000000000..91cc948fa --- /dev/null +++ b/backend/internal/nginx/control.go @@ -0,0 +1,227 @@ +package nginx + +import ( + "fmt" + "os" + + "npm/internal/config" + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/upstream" + "npm/internal/logger" + "npm/internal/status" +) + +const ( + DeletedSuffix = ".deleted" + DisabledSuffix = ".disabled" + ErrorSuffix = ".error" +) + +// ConfigureHost will attempt to write nginx conf and reload nginx +// When a host is disabled or deleted, it will name the file with a suffix +// that won't be used by nginx. +func ConfigureHost(h host.Model) error { + // nolint: errcheck, gosec + h.Expand([]string{"certificate", "nginxtemplate", "upstream"}) + + var certificateTemplate certificate.Template + if h.Certificate != nil { + certificateTemplate = h.Certificate.GetTemplate() + } + + var ups upstream.Model + if h.Upstream != nil { + ups = *h.Upstream + } + + data := TemplateData{ + Certificate: certificateTemplate, + ConfDir: fmt.Sprintf("%s/nginx/hosts", config.Configuration.DataFolder), + Config: Config{ // todo + Ipv4: !config.Configuration.DisableIPV4, + Ipv6: !config.Configuration.DisableIPV6, + }, + DataDir: config.Configuration.DataFolder, + Host: h.GetTemplate(), + Upstream: ups, + } + + removeHostFiles(h) + filename := getHostFilename(h, "") + // if h.IsDeleted { + // filename = getHostFilename(h, DeletedSuffix) + // } else if h.IsDisabled { + if h.IsDisabled { + filename = getHostFilename(h, DisabledSuffix) + } + + // Write the config to disk + err := writeTemplate(filename, h.NginxTemplate.Template, data, "") + if err != nil { + // this configuration failed somehow + h.Status = status.StatusError + h.ErrorMessage = fmt.Sprintf("Template generation failed: %s", err.Error()) + logger.Debug(h.ErrorMessage) + return h.Save(true) + } + + // Reload Nginx and check for errors + if output, err := reloadNginx(); err != nil { + // reloading nginx failed, likely due to this host having a problem + h.Status = status.StatusError + h.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s - %s", err.Error(), output) + + // Write the .error file, if this isn't a deleted or disabled host + // as the reload will only fail because of this host, if it's enabled + if !h.IsDisabled { + filename = getHostFilename(h, ErrorSuffix) + // Clear existing file(s) again + removeHostFiles(h) + // Write the template again, but with an error message at the end of the file + // nolint: errcheck, gosec + writeTemplate(filename, h.NginxTemplate.Template, data, h.ErrorMessage) + } + + logger.Debug(h.ErrorMessage) + } else { + // All good + h.Status = status.StatusOK + h.ErrorMessage = "" + logger.Debug("ConfigureHost OK: %+v", h) + } + + return h.Save(true) +} + +// ConfigureUpstream will attempt to write nginx conf and reload nginx +func ConfigureUpstream(u upstream.Model) error { + logger.Debug("ConfigureUpstream: %+v)", u) + + // nolint: errcheck, gosec + u.Expand([]string{"nginxtemplate"}) + + data := TemplateData{ + ConfDir: fmt.Sprintf("%s/nginx/upstreams", config.Configuration.DataFolder), + DataDir: config.Configuration.DataFolder, + Upstream: u, + } + + removeUpstreamFiles(u) + filename := getUpstreamFilename(u, "") + // if u.IsDeleted { + // filename = getUpstreamFilename(u, DeletedSuffix) + // } + + // Write the config to disk + err := writeTemplate(filename, u.NginxTemplate.Template, data, "") + if err != nil { + // this configuration failed somehow + u.Status = status.StatusError + u.ErrorMessage = fmt.Sprintf("Template generation failed: %s", err.Error()) + logger.Debug(u.ErrorMessage) + return u.Save(true) + } + + // nolint: errcheck, gosec + if output, err := reloadNginx(); err != nil { + // reloading nginx failed, likely due to this host having a problem + u.Status = status.StatusError + u.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s - %s", err.Error(), output) + + // Write the .error file, if this isn't a deleted upstream + // as the reload will only fail because of this upstream + // if !u.IsDeleted { + filename = getUpstreamFilename(u, ErrorSuffix) + // Clear existing file(s) again + removeUpstreamFiles(u) + // Write the template again, but with an error message at the end of the file + // nolint: errcheck, gosec + writeTemplate(filename, u.NginxTemplate.Template, data, u.ErrorMessage) + // } + + logger.Debug(u.ErrorMessage) + } else { + // All good + u.Status = status.StatusOK + u.ErrorMessage = "" + logger.Debug("ConfigureUpstream OK: %+v", u) + } + + return u.Save(true) +} + +func getHostFilename(h host.Model, appends string) string { + confDir := fmt.Sprintf("%s/nginx/hosts", config.Configuration.DataFolder) + return fmt.Sprintf("%s/host_%d.conf%s", confDir, h.ID, appends) +} + +func getUpstreamFilename(u upstream.Model, appends string) string { + confDir := fmt.Sprintf("%s/nginx/upstreams", config.Configuration.DataFolder) + return fmt.Sprintf("%s/upstream_%d.conf%s", confDir, u.ID, appends) +} + +func removeHostFiles(h host.Model) { + removeFiles([]string{ + getHostFilename(h, ""), + getHostFilename(h, DeletedSuffix), + getHostFilename(h, DisabledSuffix), + getHostFilename(h, ErrorSuffix), + }) +} + +func removeUpstreamFiles(u upstream.Model) { + removeFiles([]string{ + getUpstreamFilename(u, ""), + getUpstreamFilename(u, DeletedSuffix), + getUpstreamFilename(u, ErrorSuffix), + }) +} + +func removeFiles(files []string) { + for _, file := range files { + if _, err := os.Stat(file); err == nil { + // nolint: errcheck, gosec + os.Remove(file) + } + } +} + +// GetHostConfigContent returns nginx config as it exists on disk +func GetHostConfigContent(h host.Model) (string, error) { + filename := getHostFilename(h, "") + if h.ErrorMessage != "" { + filename = getHostFilename(h, ErrorSuffix) + } + if h.IsDisabled { + filename = getHostFilename(h, DisabledSuffix) + } + // if h.IsDeleted { + // filename = getHostFilename(h, DeletedSuffix) + // } + + // nolint: gosec + cnt, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return string(cnt), nil +} + +// GetUpstreamConfigContent returns nginx config as it exists on disk +func GetUpstreamConfigContent(u upstream.Model) (string, error) { + filename := getUpstreamFilename(u, "") + if u.ErrorMessage != "" { + filename = getUpstreamFilename(u, ErrorSuffix) + } + // if u.IsDeleted { + // filename = getUpstreamFilename(u, DeletedSuffix) + // } + + // nolint: gosec + cnt, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return string(cnt), nil +} diff --git a/backend/internal/nginx/control_test.go b/backend/internal/nginx/control_test.go new file mode 100644 index 000000000..dfbd19c67 --- /dev/null +++ b/backend/internal/nginx/control_test.go @@ -0,0 +1,52 @@ +package nginx + +import ( + "testing" + + "npm/internal/entity/host" + "npm/internal/model" + "npm/internal/test" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetHostFilename(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + test.InitConfig(t) + tests := []struct { + name string + host host.Model + append string + want string + }{ + { + "test1", + host.Model{ + Base: model.Base{ + ID: 10, + }, + }, + "", + "/data/nginx/hosts/host_10.conf", + }, + { + "test2", + host.Model{ + Base: model.Base{ + ID: 10, + }, + }, + ".deleted", + "/data/nginx/hosts/host_10.conf.deleted", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filename := getHostFilename(tt.host, tt.append) + assert.Equal(t, tt.want, filename) + }) + } +} diff --git a/backend/internal/nginx/exec.go b/backend/internal/nginx/exec.go new file mode 100644 index 000000000..81d682096 --- /dev/null +++ b/backend/internal/nginx/exec.go @@ -0,0 +1,42 @@ +package nginx + +import ( + "os/exec" + + "npm/internal/logger" + + "github.com/rotisserie/eris" +) + +func reloadNginx() (string, error) { + return shExec([]string{"-s", "reload"}) +} + +func getNginxFilePath() (string, error) { + path, err := exec.LookPath("nginx") + if err != nil { + return path, eris.Wrapf(err, "Cannot find nginx execuatable script in PATH") + } + return path, nil +} + +// shExec executes nginx with arguments +func shExec(args []string) (string, error) { + ng, err := getNginxFilePath() + if err != nil { + logger.Error("NginxError", err) + return "", err + } + + logger.Debug("CMD: %s %v", ng, args) + // nolint: gosec + c := exec.Command(ng, args...) + + b, e := c.CombinedOutput() + if e != nil { + logger.Error("NginxError", eris.Wrapf(e, "Command error: %s -- %v\n%+v", ng, args, e)) + logger.Warn(string(b)) + } + + return string(b), e +} diff --git a/backend/internal/nginx/template_test.go b/backend/internal/nginx/template_test.go new file mode 100644 index 000000000..f61c6193e --- /dev/null +++ b/backend/internal/nginx/template_test.go @@ -0,0 +1,125 @@ +package nginx + +import ( + "testing" + + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/model" + "npm/internal/test" + "npm/internal/types" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestRenderTemplate(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + test.InitConfig(t) + + template := ` +{{#if Host.IsDisabled}} + # Host is disabled +{{else}} +server { + {{#if Certificate.IsProvided}} + {{#if Certificate.IsAcme}} + include {{ConfDir}}/npm/conf.d/acme-challenge.conf; + include {{ConfDir}}/npm/conf.d/include/ssl-ciphers.conf; + {{/if}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{/if}} +} +{{/if}} +` + + type want struct { + output string + err error + } + + tests := []struct { + name string + data TemplateData + host host.Model + cert certificate.Model + want want + }{ + { + name: "Basic Template enabled", + host: host.Model{ + IsDisabled: false, + }, + cert: certificate.Model{ + Base: model.Base{ + ID: 77, + }, + Status: certificate.StatusProvided, + Type: certificate.TypeHTTP, + CertificateAuthorityID: types.NullableDBUint{Uint: 99}, + }, + want: want{ + output: ` +server { + include /etc/nginx/conf.d/npm/conf.d/acme-challenge.conf; + include /etc/nginx/conf.d/npm/conf.d/include/ssl-ciphers.conf; + ssl_certificate /data/.acme.sh/certs/npm-77/fullchain.pem; + ssl_certificate_key /data/.acme.sh/certs/npm-77/privkey.pem; +} +`, + err: nil, + }, + }, + { + name: "Basic Template custom ssl", + host: host.Model{ + IsDisabled: false, + }, + cert: certificate.Model{ + Base: model.Base{ + ID: 66, + }, + Status: certificate.StatusProvided, + Type: certificate.TypeCustom, + }, + want: want{ + output: ` +server { + ssl_certificate /data/custom_ssl/npm-66/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-66/privkey.pem; +} +`, + err: nil, + }, + }, + { + name: "Basic Template disabled", + host: host.Model{ + IsDisabled: true, + }, + cert: certificate.Model{}, + want: want{ + output: "\n # Host is disabled\n", + err: nil, + }, + }, + } + + for _, tst := range tests { + t.Run(tst.name, func(st *testing.T) { + templateData := TemplateData{ + ConfDir: "/etc/nginx/conf.d", + DataDir: "/data", + Host: tst.host.GetTemplate(), + Certificate: tst.cert.GetTemplate(), + } + + output, err := renderTemplate(template, templateData) + assert.Equal(st, tst.want.err, err) + assert.Equal(st, tst.want.output, output) + }) + } +} diff --git a/backend/internal/nginx/templates.go b/backend/internal/nginx/templates.go new file mode 100644 index 000000000..bd5276b10 --- /dev/null +++ b/backend/internal/nginx/templates.go @@ -0,0 +1,61 @@ +package nginx + +import ( + "fmt" + "os" + + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/upstream" + "npm/internal/logger" + "npm/internal/util" + + "github.com/aymerick/raymond" +) + +type Config struct { + Ipv4 bool + Ipv6 bool +} + +// TemplateData is a struct +type TemplateData struct { + ConfDir string + Config Config + DataDir string + Host host.Template + Certificate certificate.Template + Upstream upstream.Model +} + +func renderTemplate(template string, data TemplateData) (string, error) { + return raymond.Render(template, data) +} + +func writeTemplate(filename, template string, data TemplateData, errorInfo string) error { + output, err := renderTemplate(template, data) + if err != nil { + errorInfo = err.Error() + } + + output = util.CleanupWhitespace(output) + + // Write some given error information to the end + if errorInfo != "" { + output = fmt.Sprintf("%s\n\n# =========================\n# ERROR:\n# %s\n# ========================\n", output, errorInfo) + } + + // Write it. This will also write an error comment if generation failed + // nolint: gosec + writeErr := writeConfigFile(filename, output) + if err != nil { + return err + } + return writeErr +} + +func writeConfigFile(filename, content string) error { + logger.Debug("Writing %s with:\n%s", filename, content) + // nolint: gosec + return os.WriteFile(filename, []byte(content), 0644) +} diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js deleted file mode 100644 index 09b8bca51..000000000 --- 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 f22c36688..000000000 --- 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 4dde659bd..000000000 --- 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/serverevents/sse.go b/backend/internal/serverevents/sse.go new file mode 100644 index 000000000..636e91aae --- /dev/null +++ b/backend/internal/serverevents/sse.go @@ -0,0 +1,75 @@ +package serverevents + +import ( + "encoding/json" + "net/http" + + "npm/internal/logger" + + "github.com/jc21/go-sse" +) + +var instance *sse.Server + +const defaultChannel = "changes" + +// Message is how we're going to send the data +type Message struct { + Lang string `json:"lang,omitempty"` + LangParams map[string]string `json:"lang_params,omitempty"` + Type string `json:"type,omitempty"` + Affects string `json:"affects,omitempty"` +} + +// Get will return a sse server +func Get() *sse.Server { + if instance == nil { + instance = sse.NewServer(&sse.Options{ + Logger: logger.Get(), + ChannelNameFunc: func(_ *http.Request) string { + return defaultChannel // This is the channel for all updates regardless of visibility + }, + }) + } + return instance +} + +// Shutdown will shutdown the server +func Shutdown() { + if instance != nil { + instance.Shutdown() + } +} + +// SendChange will send a specific change +func SendChange(affects string) { + Send(Message{Affects: affects}, "") +} + +// SendMessage will construct a message for the UI +func SendMessage(typ, lang string, langParams map[string]string) { + Send(Message{ + Type: typ, + Lang: lang, + LangParams: langParams, + }, "") +} + +// Send will send a message +func Send(msg Message, channel string) { + if channel == "" { + channel = defaultChannel + } + logger.Debug("SSE Sending: %+v", msg) + if data, err := json.Marshal(msg); err != nil { + logger.Error("SSEError", err) + } else { + Get().SendMessage(channel, sse.SimpleMessage(string(data))) + } +} + +// TODO: if we end up implementing user visibility, +// then we'll have to subscribe people to their own +// channels and publish to all or some depending on visibility. +// This means using a specific ChannelNameFunc that revolves +// around the user and their visibility. diff --git a/backend/internal/serverevents/sse_test.go b/backend/internal/serverevents/sse_test.go new file mode 100644 index 000000000..f1353f883 --- /dev/null +++ b/backend/internal/serverevents/sse_test.go @@ -0,0 +1,27 @@ +package serverevents + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGet(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("github.com/jc21/go-sse.(*Server).dispatch")) + + s := Get() + assert.NotEqual(t, nil, s) +} + +// This is just for code coverage more than anything +func TestEverything(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t) + + Get() + SendMessage("test", "test", map[string]string{"user_id": "10"}) + SendChange("hosts") + Shutdown() +} diff --git a/backend/internal/setting.js b/backend/internal/setting.js deleted file mode 100644 index d4ac67d8a..000000000 --- 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/status/status.go b/backend/internal/status/status.go new file mode 100644 index 000000000..84f8dc6f4 --- /dev/null +++ b/backend/internal/status/status.go @@ -0,0 +1,10 @@ +package status + +const ( + // StatusReady means a host is ready to configure + StatusReady = "ready" + // StatusOK means a host is configured within Nginx + StatusOK = "ok" + // StatusError is self explanatory + StatusError = "error" +) diff --git a/backend/internal/stream.js b/backend/internal/stream.js deleted file mode 100644 index 9c458a10b..000000000 --- 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/tags/filters.go b/backend/internal/tags/filters.go new file mode 100644 index 000000000..bce6319e3 --- /dev/null +++ b/backend/internal/tags/filters.go @@ -0,0 +1,309 @@ +package tags + +import ( + "fmt" + "reflect" + "regexp" + "strings" + + "npm/internal/database" + "npm/internal/logger" + "npm/internal/model" + "npm/internal/util" + + "github.com/rotisserie/eris" +) + +// GetFilterMap ... +func GetFilterMap(m any, globalTablePrefix string) map[string]model.FilterMapValue { + name := getName(m) + filterMap := make(map[string]model.FilterMapValue) + + // 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) + + // Get the table name from the model function, if it exists + if globalTablePrefix == "" { + v := reflect.ValueOf(m) + tableNameFunc, ok := t.MethodByName("TableName") + if ok { + n := tableNameFunc.Func.Call([]reflect.Value{v}) + if len(n) > 0 { + globalTablePrefix = fmt.Sprintf( + `%s.`, + database.QuoteTableName(n[0].String()), + ) + } + } + } + + // If this is an entity model (and it probably is) + // then include the base model as well + if strings.Contains(name, ".Model") && !strings.Contains(name, "Base") { + filterMap = GetFilterMap(model.Base{}, globalTablePrefix) + } + + if t.Kind() != reflect.Struct { + logger.Error("GetFilterMapError", eris.Errorf("%v type can't have attributes inspected", t.Kind())) + return nil + } + + // 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("filter") + dbTag := field.Tag.Get("gorm") + + // Filter -> Schema mapping + if filterTag != "" && filterTag != "-" { + f := model.FilterMapValue{ + Model: name, + } + + f.Schema = getFilterTagSchema(filterTag) + parts := strings.Split(filterTag, ",") + + // Filter -> DB Field mapping + if dbTag != "" && dbTag != "-" { + // Filter tag can be a 2 part thing: name,type + // ie: account_id,integer + // So we need to split and use the first part + tablePrefix := globalTablePrefix + if len(parts) > 1 { + f.Type = parts[1] + if len(parts) > 2 { + tablePrefix = fmt.Sprintf(`"%s".`, parts[2]) + } + } + + // db can have many parts, we need to pull out the "column:value" part + f.Field = database.QuoteTableName(field.Name) + r := regexp.MustCompile(`(?:^|;)column:([^;|$]+)(?:$|;)`) + if matches := r.FindStringSubmatch(dbTag); len(matches) > 1 { + f.Field = fmt.Sprintf("%s%s", tablePrefix, database.QuoteTableName(matches[1])) + } + } + + filterMap[parts[0]] = f + } + } + + return filterMap +} + +func getFilterTagSchema(filterTag string) string { + // 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 "number": + fallthrough + case "int": + fallthrough + case "integer": + return intFieldSchema(items[0]) + case "bool": + fallthrough + case "boolean": + return boolFieldSchema(items[0]) + case "date": + return dateFieldSchema(items[0]) + case "regex": + if len(items) < 3 { + items = append(items, ".*") + } + return regexFieldSchema(items[0], items[2]) + + default: + return stringFieldSchema(items[0]) + } +} + +// GetFilterSchema creates a jsonschema for validating filters, based on the model +// object given and by reading the struct "filter" tags. +func GetFilterSchema(m any) string { + filterMap := GetFilterMap(m, "") + schemas := make([]string, 0) + + for _, f := range filterMap { + schemas = append(schemas, f.Schema) + } + + str := fmt.Sprintf(baseFilterSchema, strings.Join(schemas, ", ")) + return util.PrettyPrintJSON(str) +} + +// 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/tags/filters_test.go b/backend/internal/tags/filters_test.go new file mode 100644 index 000000000..b6685f1cb --- /dev/null +++ b/backend/internal/tags/filters_test.go @@ -0,0 +1,108 @@ +package tags + +import ( + "testing" + "time" + + "npm/internal/util" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetFilterSchema(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type testDemo struct { + ID uint `json:"id" gorm:"column:user_id" filter:"id,number"` + Created time.Time `json:"created" gorm:"column:user_created_date" filter:"created,date"` + Name string `json:"name" gorm:"column:user_name" filter:"name,string"` + IsDisabled string `json:"is_disabled" gorm:"column:user_is_disabled" filter:"is_disabled,bool"` + Permissions string `json:"permissions" gorm:"column:user_permissions" filter:"permissions,regex"` + History string `json:"history" gorm:"column:user_history" filter:"history,regex,(id|name)"` + } + + m := testDemo{ID: 10, Name: "test"} + + filterSchema := GetFilterSchema(m) + assert.Greater(t, len(filterSchema), 4000) + // Trigger again for code coverage of cached item + GetFilterSchema(m) +} + +func TestGetFilterTagSchema(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + schema := util.PrettyPrintJSON(getFilterTagSchema("id,integer")) + + expectedSchema := `{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^id$" + }, + "modifier": { + "type": "string", + "pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$" + }, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + ] + } + } +}` + + assert.Equal(t, expectedSchema, schema) +} + +func TestBoolFieldSchema(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + schema := util.PrettyPrintJSON(boolFieldSchema("active")) + + expectedSchema := `{ + "type": "object", + "properties": { + "field": { + "type": "string", + "pattern": "^active$" + }, + "modifier": { + "type": "string", + "pattern": "^(equals|not)$" + }, + "value": { + "oneOf": [ + { + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" + }, + { + "type": "array", + "items": { + "type": "string", + "pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$" + } + } + ] + } + } +}` + + assert.Equal(t, expectedSchema, schema) +} diff --git a/backend/internal/tags/reflect.go b/backend/internal/tags/reflect.go new file mode 100644 index 000000000..0dbadbee2 --- /dev/null +++ b/backend/internal/tags/reflect.go @@ -0,0 +1,33 @@ +package tags + +import ( + "fmt" + "reflect" + + "npm/internal/model" +) + +var tagCache map[string]map[string]model.FilterMapValue + +// getName returns the name of the type given +func getName(m any) string { + fc := reflect.TypeOf(m) + return fmt.Sprint(fc) +} + +// getCache tries to find a cached item for this name +func getCache(name string) (map[string]model.FilterMapValue, bool) { + if val, ok := tagCache[name]; ok { + return val, true + } + return nil, false +} + +// setCache sets the name to this value +func setCache(name string, val map[string]model.FilterMapValue) { + // Hack to initialise empty map + if len(tagCache) == 0 { + tagCache = make(map[string]map[string]model.FilterMapValue, 0) + } + tagCache[name] = val +} diff --git a/backend/internal/tags/reflect_test.go b/backend/internal/tags/reflect_test.go new file mode 100644 index 000000000..e29c59855 --- /dev/null +++ b/backend/internal/tags/reflect_test.go @@ -0,0 +1,46 @@ +package tags + +import ( + "testing" + + "npm/internal/model" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestGetName(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + type testDemo struct { + UserID uint `json:"user_id" gorm:"column:user_id" filter:"user_id,integer"` + Type string `json:"type" gorm:"column:type" filter:"type,string"` + } + + m := testDemo{UserID: 10, Type: "test"} + assert.Equal(t, "tags.testDemo", getName(m)) +} + +func TestCache(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + name := "testdemo" + // Should return error + _, exists := getCache(name) + assert.Equal(t, false, exists) + + setCache(name, map[string]model.FilterMapValue{ + "test": { + Field: "test", + Type: "test", + }, + }) + + // Should return value + val, exists := getCache(name) + assert.Equal(t, true, exists) + assert.Equal(t, "test", val["test"].Field) + assert.Equal(t, "test", val["test"].Type) +} diff --git a/backend/internal/test/suite.go b/backend/internal/test/suite.go new file mode 100644 index 000000000..8d9fec23d --- /dev/null +++ b/backend/internal/test/suite.go @@ -0,0 +1,44 @@ +package test + +import ( + "strings" + "testing" + + "npm/internal/config" + "npm/internal/database" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// Setup ... +func Setup() (sqlmock.Sqlmock, error) { + db, mock, err := sqlmock.New() + if err != nil { + return nil, err + } + dialector := postgres.New(postgres.Config{ + Conn: db, + DriverName: "postgres", + }) + gormDB, err := gorm.Open(dialector, &gorm.Config{}) + database.SetDB(gormDB) + return mock, err +} + +// InitConfig ... +func InitConfig(t *testing.T, envs ...string) { + if len(envs) > 0 { + for _, env := range envs { + parts := strings.Split(env, "=") + if len(parts) == 2 { + t.Setenv(parts[0], parts[1]) + } + } + } + + version := "999.999.999" + commit := "abcd123" + config.Init(&version, &commit) +} diff --git a/backend/internal/token.js b/backend/internal/token.js deleted file mode 100644 index a64b90105..000000000 --- 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 000000000..27e212df5 --- /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 any) 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/db_date_test.go b/backend/internal/types/db_date_test.go new file mode 100644 index 000000000..1aeafec11 --- /dev/null +++ b/backend/internal/types/db_date_test.go @@ -0,0 +1,124 @@ +package types + +import ( + "encoding/json" + "testing" + "time" + + "go.uber.org/goleak" +) + +func TestDBDate_Value(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Create a DBDate instance with a specific time + expectedTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + dbDate := DBDate{Time: expectedTime} + + // Call the Value method + value, err := dbDate.Value() + + // Assert the value and error + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Convert the value to int64 + unixTime := value.(int64) + + // Convert the unix time back to time.Time + actualTime := time.Unix(unixTime, 0) + + // Compare the actual time with the expected time + if !actualTime.Equal(expectedTime) { + t.Errorf("Expected time '%v', got '%v'", expectedTime, actualTime) + } +} + +func TestDBDate_Scan(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Simulate a value from the database (unix timestamp) + unixTime := int64(1640995200) + + // Create a DBDate instance + dbDate := DBDate{} + + // Call the Scan method + err := dbDate.Scan(unixTime) + + // Assert the error + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Convert the DBDate's time to unix timestamp for comparison + actualUnixTime := dbDate.Time.Unix() + + // Compare the actual unix time with the expected unix time + if actualUnixTime != unixTime { + t.Errorf("Expected unix time '%v', got '%v'", unixTime, actualUnixTime) + } +} + +func TestDBDate_UnmarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Simulate a JSON input representing a unix timestamp + jsonData := []byte("1640995200") + + // Create a DBDate instance + dbDate := DBDate{} + + // Call the UnmarshalJSON method + err := dbDate.UnmarshalJSON(jsonData) + + // Assert the error + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Convert the DBDate's time to unix timestamp for comparison + actualUnixTime := dbDate.Time.Unix() + + // Compare the actual unix time with the expected unix time + expectedUnixTime := int64(1640995200) + if actualUnixTime != expectedUnixTime { + t.Errorf("Expected unix time '%v', got '%v'", expectedUnixTime, actualUnixTime) + } +} + +func TestDBDate_MarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + // Create a DBDate instance with a specific time + expectedTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + dbDate := DBDate{Time: expectedTime} + + // Call the MarshalJSON method + jsonData, err := dbDate.MarshalJSON() + + // Assert the value and error + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Convert the JSON data to an integer + var actualUnixTime int64 + err = json.Unmarshal(jsonData, &actualUnixTime) + if err != nil { + t.Errorf("Failed to unmarshal JSON data: %v", err) + } + + // Convert the unix time back to time.Time + actualTime := time.Unix(actualUnixTime, 0) + + // Compare the actual time with the expected time + if !actualTime.Equal(expectedTime) { + t.Errorf("Expected time '%v', got '%v'", expectedTime, actualTime) + } +} diff --git a/backend/internal/types/db_nullable_int.go b/backend/internal/types/db_nullable_int.go new file mode 100644 index 000000000..0ece3f5d8 --- /dev/null +++ b/backend/internal/types/db_nullable_int.go @@ -0,0 +1,63 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "strconv" +) + +// NullableDBInt works with database values that can be null or an integer value +type NullableDBInt struct { + Int int +} + +// Value encodes the type ready for the database +func (d NullableDBInt) Value() (driver.Value, error) { + if d.Int == 0 { + return nil, nil + } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` + return driver.Value(int64(d.Int)), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBInt) Scan(src any) error { + var i int + switch v := src.(type) { + case int: + i = v + case int64: + i = int(v) + case float32: + i = int(v) + case float64: + i = int(v) + case string: + i, _ = strconv.Atoi(v) + } + d.Int = i + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBInt) UnmarshalJSON(data []byte) error { + // total_deploy_time: 10, + // total_deploy_time: null, + + var i int + if err := json.Unmarshal(data, &i); err != nil { + i = 0 + return nil + } + d.Int = i + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBInt) MarshalJSON() ([]byte, error) { + if d.Int == 0 { + return json.Marshal(nil) + } + return json.Marshal(d.Int) +} diff --git a/backend/internal/types/db_nullable_int_test.go b/backend/internal/types/db_nullable_int_test.go new file mode 100644 index 000000000..13f8558f7 --- /dev/null +++ b/backend/internal/types/db_nullable_int_test.go @@ -0,0 +1,105 @@ +package types + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestNullableDBIntValue(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var d NullableDBInt + + // Test when Int is 0 (null) + d.Int = 0 + value, err := d.Value() + if value != nil || err != nil { + t.Errorf("Expected Value() to return nil, nil but got %v, %v", value, err) + } + + // Test when Int is not null + d.Int = 10 + value, err = d.Value() + if value != int64(10) || err != nil { + t.Errorf("Expected Value() to return 10, nil but got %v, %v", value, err) + } +} + +func TestNullableDBIntScan(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var d NullableDBInt + + // Test when src is an int + err := d.Scan(20) + if d.Int != 20 || err != nil { + t.Errorf("Expected Scan(20) to set d.Int to 20 and return nil but got d.Int = %d, err = %v", d.Int, err) + } + + // Test when src is an int64 + err = d.Scan(int64(30)) + if d.Int != 30 || err != nil { + t.Errorf("Expected Scan(int64(30)) to set d.Int to 30 and return nil but got d.Int = %d, err = %v", d.Int, err) + } + + // Test when src is a float32 + err = d.Scan(float32(40)) + if d.Int != 40 || err != nil { + t.Errorf("Expected Scan(float32(40)) to set d.Int to 40 and return nil but got d.Int = %d, err = %v", d.Int, err) + } + + // Test when src is a float64 + err = d.Scan(float64(50)) + if d.Int != 50 || err != nil { + t.Errorf("Expected Scan(float64(50)) to set d.Int to 50 and return nil but got d.Int = %d, err = %v", d.Int, err) + } + + // Test when src is a string + err = d.Scan("60") + if d.Int != 60 || err != nil { + t.Errorf("Expected Scan(\"60\") to set d.Int to 60 and return nil but got d.Int = %d, err = %v", d.Int, err) + } +} + +func TestNullableDBIntUnmarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var d NullableDBInt + + // Test when data is an integer value + err := d.UnmarshalJSON([]byte("10")) + if d.Int != 10 || err != nil { + t.Errorf("Expected UnmarshalJSON([]byte(\"10\")) to set d.Int to 10 and return nil but got d.Int = %d, err = %v", d.Int, err) + } + + // Test when data is null + err = d.UnmarshalJSON([]byte("null")) + if d.Int != 0 || err != nil { + t.Errorf("Expected UnmarshalJSON([]byte(\"null\")) to set d.Int to 0 and return nil but got d.Int = %d, err = %v", d.Int, err) + } +} + +func TestNullableDBIntMarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var d NullableDBInt + + // Test when Int is 0 (null) + d.Int = 0 + result, err := d.MarshalJSON() + if string(result) != "null" || err != nil { + t.Errorf("Expected MarshalJSON() to return \"null\", nil but got %s, %v", result, err) + } + + // Test when Int is not null + d.Int = 10 + result, err = d.MarshalJSON() + if string(result) != "10" || err != nil { + t.Errorf("Expected MarshalJSON() to return \"10\", nil but got %s, %v", result, err) + } +} diff --git a/backend/internal/types/db_nullable_uint.go b/backend/internal/types/db_nullable_uint.go new file mode 100644 index 000000000..e268621d7 --- /dev/null +++ b/backend/internal/types/db_nullable_uint.go @@ -0,0 +1,68 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + "strconv" +) + +// NullableDBUint works with database values that can be null or an integer value +type NullableDBUint struct { + Uint uint +} + +// Value encodes the type ready for the database +func (d NullableDBUint) Value() (driver.Value, error) { + if d.Uint == 0 { + return nil, nil + } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` + // nolint: gosec + return driver.Value(int64(d.Uint)), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBUint) Scan(src any) error { + var i uint + switch v := src.(type) { + case int: + // nolint: gosec + i = uint(v) + case int64: + // nolint: gosec + i = uint(v) + case float32: + i = uint(v) + case float64: + i = uint(v) + case string: + a, _ := strconv.Atoi(v) + // nolint: gosec + i = uint(a) + } + d.Uint = i + return nil +} + +// UnmarshalJSON will unmarshal both database and post given values +func (d *NullableDBUint) UnmarshalJSON(data []byte) error { + // total_deploy_time: 10, + // total_deploy_time: null, + + var i uint + if err := json.Unmarshal(data, &i); err != nil { + i = 0 + return nil + } + d.Uint = i + return nil +} + +// MarshalJSON will marshal for output in api responses +func (d NullableDBUint) MarshalJSON() ([]byte, error) { + if d.Uint == 0 { + return json.Marshal(nil) + } + return json.Marshal(d.Uint) +} diff --git a/backend/internal/types/db_nullable_uint_test.go b/backend/internal/types/db_nullable_uint_test.go new file mode 100644 index 000000000..129f91a77 --- /dev/null +++ b/backend/internal/types/db_nullable_uint_test.go @@ -0,0 +1,160 @@ +package types + +import ( + "database/sql/driver" + "testing" + + "go.uber.org/goleak" +) + +func TestNullableDBUint_Value(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + input NullableDBUint + wantValue driver.Value + wantErr bool + }{ + { + name: "Value should return nil when Uint is 0", + input: NullableDBUint{Uint: 0}, + wantValue: nil, + wantErr: false, + }, + { + name: "Value should return int64 value of Uint", + input: NullableDBUint{Uint: 10}, + wantValue: int64(10), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotErr := tt.input.Value() + if gotValue != tt.wantValue { + t.Errorf("Value() = %v, want %v", gotValue, tt.wantValue) + } + if (gotErr != nil) != tt.wantErr { + t.Errorf("Value() error = %v, wantErr %v", gotErr, tt.wantErr) + } + }) + } +} + +func TestNullableDBUint_Scan(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + input any + wantUint uint + wantErr bool + }{ + { + name: "Scan should convert int to uint", + input: int(10), + wantUint: uint(10), + wantErr: false, + }, + { + name: "Scan should convert int64 to uint", + input: int64(10), + wantUint: uint(10), + wantErr: false, + }, + // Add more tests for other supported types + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var d NullableDBUint + err := d.Scan(tt.input) + if err != nil && !tt.wantErr { + t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) + } + if d.Uint != tt.wantUint { + t.Errorf("Scan() Uint = %v, want %v", d.Uint, tt.wantUint) + } + }) + } +} + +func TestNullableDBUint_UnmarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + input []byte + wantUint uint + wantErr bool + }{ + { + name: "UnmarshalJSON should unmarshal integer value", + input: []byte("10"), + wantUint: uint(10), + wantErr: false, + }, + { + name: "UnmarshalJSON should return zero Uint when data is invalid", + input: []byte(`"invalid"`), + wantUint: uint(0), + wantErr: false, + }, + // Add more tests for other scenarios + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var d NullableDBUint + err := d.UnmarshalJSON(tt.input) + if err != nil && !tt.wantErr { + t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + } + if d.Uint != tt.wantUint { + t.Errorf("UnmarshalJSON() Uint = %v, want %v", d.Uint, tt.wantUint) + } + }) + } +} + +func TestNullableDBUint_MarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + input NullableDBUint + wantOutput []byte + wantErr bool + }{ + { + name: "MarshalJSON should marshal nil when Uint is 0", + input: NullableDBUint{Uint: 0}, + wantOutput: []byte("null"), + wantErr: false, + }, + { + name: "MarshalJSON should marshal Uint as JSON value", + input: NullableDBUint{Uint: 10}, + wantOutput: []byte("10"), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotOutput, gotErr := tt.input.MarshalJSON() + if (gotErr != nil) != tt.wantErr { + t.Errorf("MarshalJSON() error = %v, wantErr %v", gotErr, tt.wantErr) + } + if string(gotOutput) != string(tt.wantOutput) { + t.Errorf("MarshalJSON() output = %s, want %s", gotOutput, tt.wantOutput) + } + }) + } +} diff --git a/backend/internal/types/jsonb.go b/backend/internal/types/jsonb.go new file mode 100644 index 000000000..1cd8b3b7b --- /dev/null +++ b/backend/internal/types/jsonb.go @@ -0,0 +1,72 @@ +package types + +import ( + "database/sql/driver" + "encoding/json" + + "github.com/rotisserie/eris" +) + +// JSONB can be anything +type JSONB struct { + Encoded string `json:"decoded"` + Decoded any `json:"encoded"` +} + +// Value encodes the type ready for the database +func (j JSONB) Value() (driver.Value, error) { + jsn, err := json.Marshal(j.Decoded) + return driver.Value(string(jsn)), err +} + +// Scan takes data from the database and modifies it for Go Types +func (j *JSONB) Scan(src any) error { + var jsonb JSONB + var srcString string + switch v := src.(type) { + case string: + srcString = src.(string) + case []uint8: + srcString = string(src.([]uint8)) + default: + return eris.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) +} + +// AsStringArray will attempt to return as []string +func (j JSONB) AsStringArray() ([]string, error) { + var strs []string + + // Encode then Decode onto this type + b, _ := j.MarshalJSON() + if err := json.Unmarshal(b, &strs); err != nil { + return strs, err + } + + return strs, nil +} diff --git a/backend/internal/types/jsonb_test.go b/backend/internal/types/jsonb_test.go new file mode 100644 index 000000000..0ec7e55c5 --- /dev/null +++ b/backend/internal/types/jsonb_test.go @@ -0,0 +1,133 @@ +package types + +import ( + "encoding/json" + "testing" +) + +// TestJSONBValue tests the Value method of the JSONB type +func TestJSONBValue(t *testing.T) { + j := JSONB{ + Decoded: map[string]any{ + "name": "John", + "age": 30, + }, + } + + value, err := j.Value() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // nolint: goconst + if value != `{"age":30,"name":"John"}` { + t.Errorf("Incorrect value. Expected: %s, Got: %s", `{"name":"John","age":30}`, value) + } +} + +// TestJSONBScan tests the Scan method of the JSONB type +func TestJSONBScan(t *testing.T) { + src := `{"name":"John","age":30}` + var j JSONB + + err := j.Scan(src) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedDecoded := map[string]any{ + "name": "John", + "age": 30, + } + + if !jsonEqual(j.Decoded, expectedDecoded) { + t.Errorf("Incorrect decoded value. Expected: %v, Got: %v", expectedDecoded, j.Decoded) + } + + if j.Encoded != src { + t.Errorf("Incorrect encoded value. Expected: %s, Got: %s", src, j.Encoded) + } +} + +// TestJSONBUnmarshalJSON tests the UnmarshalJSON method of the JSONB type +func TestJSONBUnmarshalJSON(t *testing.T) { + data := []byte(`{"name":"John","age":30}`) + var j JSONB + + err := j.UnmarshalJSON(data) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedDecoded := map[string]any{ + "name": "John", + "age": 30, + } + + if !jsonEqual(j.Decoded, expectedDecoded) { + t.Errorf("Incorrect decoded value. Expected: %v, Got: %v", expectedDecoded, j.Decoded) + } + + if j.Encoded != string(data) { + t.Errorf("Incorrect encoded value. Expected: %s, Got: %s", string(data), j.Encoded) + } +} + +// TestJSONBMarshalJSON tests the MarshalJSON method of the JSONB type +func TestJSONBMarshalJSON(t *testing.T) { + j := JSONB{ + Decoded: map[string]any{ + "name": "John", + "age": 30, + }, + } + + result, err := j.MarshalJSON() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedResult := `{"age":30,"name":"John"}` + + if string(result) != expectedResult { + t.Errorf("Incorrect result. Expected: %s, Got: %s", expectedResult, string(result)) + } +} + +// TestJSONBAsStringArray tests the AsStringArray method of the JSONB type +func TestJSONBAsStringArray(t *testing.T) { + j := JSONB{ + Decoded: []string{"apple", "banana", "orange"}, + } + + strs, err := j.AsStringArray() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedStrs := []string{"apple", "banana", "orange"} + + if !stringSliceEqual(strs, expectedStrs) { + t.Errorf("Incorrect result. Expected: %v, Got: %v", expectedStrs, strs) + } +} + +// Helper function to compare JSON objects +func jsonEqual(a, b any) bool { + aJSON, _ := json.Marshal(a) + bJSON, _ := json.Marshal(b) + return string(aJSON) == string(bJSON) +} + +// Helper function to compare string slices +func stringSliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/backend/internal/types/nullable_db_date.go b/backend/internal/types/nullable_db_date.go new file mode 100644 index 000000000..f6532ee90 --- /dev/null +++ b/backend/internal/types/nullable_db_date.go @@ -0,0 +1,74 @@ +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 + } + // According to current database/sql docs, the sql has four builtin functions that + // returns driver.Value, and the underlying types are `int64`, `float64`, `string` and `bool` + return driver.Value(d.Time.Unix()), nil +} + +// Scan takes data from the database and modifies it for Go Types +func (d *NullableDBDate) Scan(src any) 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()) +} + +// AsInt64 will attempt to return a unixtime +func (d NullableDBDate) AsInt64() int64 { + if d.Time == nil || d.Time.IsZero() { + return 0 + } + + return d.Time.Unix() +} + +// AsString returns date as a string +func (d NullableDBDate) AsString() string { + if d.Time == nil || d.Time.IsZero() { + return "" + } + + return d.Time.String() +} diff --git a/backend/internal/types/nullable_db_date_test.go b/backend/internal/types/nullable_db_date_test.go new file mode 100644 index 000000000..613f3fa07 --- /dev/null +++ b/backend/internal/types/nullable_db_date_test.go @@ -0,0 +1,127 @@ +package types + +import ( + "testing" + "time" + + "go.uber.org/goleak" +) + +// TestNullableDBDateValue tests the Value method of the NullableDBDate type +func TestNullableDBDateValue(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tme := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + d := NullableDBDate{ + Time: &tme, + } + + value, err := d.Value() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedValue := tme.Unix() + + if value != expectedValue { + t.Errorf("Incorrect value. Expected: %d, Got: %v", expectedValue, value) + } +} + +// TestNullableDBDateScan tests the Scan method of the NullableDBDate type +func TestNullableDBDateScan(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var d NullableDBDate + + err := d.Scan(int64(1640995200)) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + + if !expectedTime.Equal(*d.Time) { + t.Errorf("Incorrect time. Expected: %v, Got: %v", expectedTime, *d.Time) + } +} + +// TestNullableDBDateUnmarshalJSON tests the UnmarshalJSON method of the NullableDBDate type +func TestNullableDBDateUnmarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + data := []byte(`1640995200`) + var d NullableDBDate + + err := d.UnmarshalJSON(data) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + + if !expectedTime.Equal(*d.Time) { + t.Errorf("Incorrect time. Expected: %v, Got: %v", expectedTime, *d.Time) + } +} + +// TestNullableDBDateMarshalJSON tests the MarshalJSON method of the NullableDBDate type +func TestNullableDBDateMarshalJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tme := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + d := NullableDBDate{ + Time: &tme, + } + + result, err := d.MarshalJSON() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expectedResult := []byte(`1640995200`) + + if string(result) != string(expectedResult) { + t.Errorf("Incorrect result. Expected: %s, Got: %s", expectedResult, result) + } +} + +// TestNullableDBDateAsInt64 tests the AsInt64 method of the NullableDBDate type +func TestNullableDBDateAsInt64(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tme := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + d := NullableDBDate{ + Time: &tme, + } + + unixtime := d.AsInt64() + expectedUnixtime := tme.Unix() + + if unixtime != expectedUnixtime { + t.Errorf("Incorrect unixtime. Expected: %d, Got: %d", expectedUnixtime, unixtime) + } +} + +// TestNullableDBDateAsString tests the AsString method of the NullableDBDate type +func TestNullableDBDateAsString(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tme := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) + d := NullableDBDate{ + Time: &tme, + } + + str := d.AsString() + expectedStr := tme.String() + + if str != expectedStr { + t.Errorf("Incorrect string. Expected: %s, Got: %s", expectedStr, str) + } +} diff --git a/backend/internal/user.js b/backend/internal/user.js deleted file mode 100644 index 2e2d8abf6..000000000 --- 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/interfaces.go b/backend/internal/util/interfaces.go new file mode 100644 index 000000000..d647dcb6e --- /dev/null +++ b/backend/internal/util/interfaces.go @@ -0,0 +1,36 @@ +package util + +// FindItemInInterface Find key in interface (recursively) and return value as interface +func FindItemInInterface(key string, obj any) (any, bool) { + // if the argument is not a map, ignore it + mobj, ok := obj.(map[string]any) + if !ok { + return nil, false + } + + for k, v := range mobj { + // key match, return value + if k == key { + return v, true + } + + // if the value is a map, search recursively + if m, ok := v.(map[string]any); ok { + if res, ok := FindItemInInterface(key, m); ok { + return res, true + } + } + // if the value is an array, search recursively + // from each element + if va, ok := v.([]any); ok { + for _, a := range va { + if res, ok := FindItemInInterface(key, a); ok { + return res, true + } + } + } + } + + // element not found + return nil, false +} diff --git a/backend/internal/util/interfaces_test.go b/backend/internal/util/interfaces_test.go new file mode 100644 index 000000000..c2b480235 --- /dev/null +++ b/backend/internal/util/interfaces_test.go @@ -0,0 +1,37 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestFindItemInInterface(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + obj := map[string]any{ + "key1": "value1", + "key2": 10, + "key3": map[string]any{ + "nestedKey": "nestedValue", + }, + "key4": []any{"item1", "item2"}, + } + + // Test case 1: Key exists at the top level + result, found := FindItemInInterface("key1", obj) + assert.Equal(t, true, found) + assert.Equal(t, "value1", result) + + // Test case 2: Key exists at a nested level + result, found = FindItemInInterface("nestedKey", obj) + assert.Equal(t, true, found) + assert.Equal(t, "nestedValue", result) + + // Test case 3: Key does not exist + result, found = FindItemInInterface("nonExistentKey", obj) + assert.Equal(t, false, found) + assert.Equal(t, nil, result) +} diff --git a/backend/internal/util/maps.go b/backend/internal/util/maps.go new file mode 100644 index 000000000..a2cd27883 --- /dev/null +++ b/backend/internal/util/maps.go @@ -0,0 +1,9 @@ +package util + +// MapContainsKey is fairly self explanatory +func MapContainsKey(dict map[string]any, 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 000000000..0d1bc621a --- /dev/null +++ b/backend/internal/util/maps_test.go @@ -0,0 +1,49 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +type rect struct { + width int + height int +} + +func TestMapContainsKey(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + var r rect + r.width = 5 + r.height = 5 + m := map[string]any{ + "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 000000000..b02cd3463 --- /dev/null +++ b/backend/internal/util/slices.go @@ -0,0 +1,44 @@ +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, ",") +} + +// ConvertStringSliceToInterface is required in some special cases +func ConvertStringSliceToInterface(slice []string) []any { + res := make([]any, len(slice)) + for i := range slice { + res[i] = slice[i] + } + return res +} diff --git a/backend/internal/util/slices_test.go b/backend/internal/util/slices_test.go new file mode 100644 index 000000000..4afee77f4 --- /dev/null +++ b/backend/internal/util/slices_test.go @@ -0,0 +1,115 @@ +package util + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestSliceContainsItem(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + 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) +} + +func TestConvertStringSliceToInterface(t *testing.T) { + testCases := []struct { + input []string + expected []any + }{ + {[]string{"hello", "world"}, []any{"hello", "world"}}, + {[]string{"apple", "banana", "cherry"}, []any{"apple", "banana", "cherry"}}, + {[]string{}, []any{}}, // Empty slice should return an empty slice + } + + for _, tc := range testCases { + result := ConvertStringSliceToInterface(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected: %v, Got: %v", tc.expected, result) + } + } +} diff --git a/backend/internal/util/strings.go b/backend/internal/util/strings.go new file mode 100644 index 000000000..c3e9b70a0 --- /dev/null +++ b/backend/internal/util/strings.go @@ -0,0 +1,41 @@ +package util + +import ( + "bytes" + "encoding/json" + "regexp" + "strings" + "unicode" + + "npm/internal/logger" +) + +// CleanupWhitespace will trim up and remove extra lines and stuff +func CleanupWhitespace(s string) string { + // Remove trailing whitespace from all lines + slices := strings.Split(s, "\n") + for idx := range slices { + slices[idx] = strings.TrimRightFunc(slices[idx], unicode.IsSpace) + } + // Output: [a b c] + result := strings.Join(slices, "\n") + + // Remove empty lines + r1 := regexp.MustCompile("\n+") + result = r1.ReplaceAllString(result, "\n") + + return result +} + +// PrettyPrintJSON takes a string and as long as it's JSON, +// it will return a pretty printed and formatted version +func PrettyPrintJSON(s string) string { + byt := []byte(s) + var prettyJSON bytes.Buffer + if err := json.Indent(&prettyJSON, byt, "", " "); err != nil { + logger.Debug("Can't pretty print non-json string: %s", s) + return s + } + + return prettyJSON.String() +} diff --git a/backend/internal/util/strings_test.go b/backend/internal/util/strings_test.go new file mode 100644 index 000000000..cb289f0b1 --- /dev/null +++ b/backend/internal/util/strings_test.go @@ -0,0 +1,74 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestCleanupWhitespace(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + name string + input string + want string + }{ + { + name: "test a", + input: `# ------------------------------------------------------------ +# Upstream 5: API servers 2 +# ------------------------------------------------------------ + +upstream npm_upstream_5 {` + ` ` + /* this adds whitespace to the end without the ide trimming it */ ` + + + + + + + + + + server 192.168.0.10:80 weight=100 ; + server 192.168.0.11:80 weight=50 ; + +}`, + want: `# ------------------------------------------------------------ +# Upstream 5: API servers 2 +# ------------------------------------------------------------ +upstream npm_upstream_5 { + server 192.168.0.10:80 weight=100 ; + server 192.168.0.11:80 weight=50 ; +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := CleanupWhitespace(tt.input) + assert.Equal(t, tt.want, output) + }) + } +} + +func TestPrettyPrintJSON(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + testCases := []struct { + input string + expected string + }{ + {`{"name":"John","age":30,"city":"New York"}`, "{\n \"name\": \"John\",\n \"age\": 30,\n \"city\": \"New York\"\n}"}, + {`{"fruit":"apple","color":"red"}`, "{\n \"fruit\": \"apple\",\n \"color\": \"red\"\n}"}, + {"invalid-json", "invalid-json"}, // non-JSON input should return the original string unchanged + } + + for _, tc := range testCases { + result := PrettyPrintJSON(tc.input) + assert.Equal(t, tc.expected, result) + } +} diff --git a/backend/internal/util/time.go b/backend/internal/util/time.go new file mode 100644 index 000000000..b1aed5887 --- /dev/null +++ b/backend/internal/util/time.go @@ -0,0 +1,9 @@ +package util + +import "time" + +// UnixMilliToNiceFormat converts a millisecond to nice string +func UnixMilliToNiceFormat(milli int64) string { + t := time.Unix(0, milli*int64(time.Millisecond)) + return t.Format("2006-01-02 15:04:05") +} diff --git a/backend/internal/util/time_test.go b/backend/internal/util/time_test.go new file mode 100644 index 000000000..d15c7e499 --- /dev/null +++ b/backend/internal/util/time_test.go @@ -0,0 +1,29 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestUnixMilliToNiceFormat(t *testing.T) { + // goleak is used to detect goroutine leaks + defer goleak.VerifyNone(t, goleak.IgnoreAnyFunction("database/sql.(*DB).connectionOpener")) + + tests := []struct { + input int64 + expected string + }{ + {0, "1970-01-01 10:00:00"}, // Unix epoch time + {1568000000000, "2019-09-09 13:33:20"}, // Arbitrary millisecond timestamp + {1636598400000, "2021-11-11 12:40:00"}, // Another arbitrary millisecond timestamp + {-1000000000000, "1938-04-25 08:13:20"}, // Negative millisecond timestamp + {9223372036854775807, "1970-01-01 09:59:59"}, // Maximum representable millisecond timestamp + } + + for _, test := range tests { + output := UnixMilliToNiceFormat(test.input) + assert.Equal(t, test.expected, output) + } +} diff --git a/backend/internal/validator/hosts.go b/backend/internal/validator/hosts.go new file mode 100644 index 000000000..685a18b43 --- /dev/null +++ b/backend/internal/validator/hosts.go @@ -0,0 +1,55 @@ +package validator + +import ( + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" + + "github.com/rotisserie/eris" +) + +var ( + certificateGetByID = certificate.GetByID + upstreamGetByID = upstream.GetByID + nginxtemplateGetByID = nginxtemplate.GetByID +) + +// 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.Uint > 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 := certificateGetByID(h.CertificateID.Uint); cErr != nil { + return eris.Wrapf(cErr, "Certificate #%d does not exist", h.CertificateID.Uint) + } + } + + if h.UpstreamID.Uint > 0 { + // Check upstream exists + if _, uErr := upstreamGetByID(h.UpstreamID.Uint); uErr != nil { + return eris.Wrapf(uErr, "Upstream #%d does not exist", h.UpstreamID.Uint) + } + } + + // Ensure either UpstreamID is set or appropriate proxy host params are set + if h.UpstreamID.Uint > 0 && (h.ProxyHost != "" || h.ProxyPort > 0) { + return eris.Errorf("Proxy Host or Port cannot be set when using an Upstream") + } + if h.UpstreamID.Uint == 0 && (h.ProxyHost == "" || h.ProxyPort < 1) { + return eris.Errorf("Proxy Host and Port must be specified, unless using an Upstream") + } + + // Check the nginx template exists and has the same type. + nginxTemplate, tErr := nginxtemplateGetByID(h.NginxTemplateID) + if tErr != nil { + return eris.Wrapf(tErr, "Host Template #%d does not exist", h.NginxTemplateID) + } + if nginxTemplate.Type != h.Type { + return eris.Errorf("Host Template #%d is not valid for this host type", h.NginxTemplateID) + } + + return nil +} diff --git a/backend/internal/validator/hosts_test.go b/backend/internal/validator/hosts_test.go new file mode 100644 index 000000000..946762925 --- /dev/null +++ b/backend/internal/validator/hosts_test.go @@ -0,0 +1,145 @@ +package validator + +import ( + "testing" + + "npm/internal/entity/certificate" + "npm/internal/entity/host" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" + "npm/internal/types" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +// Mocking the dependencies +type MockCertificate struct { + mock.Mock +} + +func (m *MockCertificate) GetByID(id uint) (certificate.Model, error) { + args := m.Called(id) + return args.Get(0).(certificate.Model), args.Error(1) +} + +type MockUpstream struct { + mock.Mock +} + +func (m *MockUpstream) GetByID(id uint) (upstream.Model, error) { + args := m.Called(id) + return args.Get(0).(upstream.Model), args.Error(1) +} + +type MockNginxTemplate struct { + mock.Mock +} + +func (m *MockNginxTemplate) GetByID(id uint) (nginxtemplate.Model, error) { + args := m.Called(id) + return args.Get(0).(nginxtemplate.Model), args.Error(1) +} + +func TestValidateHost(t *testing.T) { + tests := []struct { + name string + host host.Model + wantErr string + }{ + { + name: "valid host with certificate and upstream", + host: host.Model{ + CertificateID: types.NullableDBUint{Uint: 1}, + UpstreamID: types.NullableDBUint{Uint: 1}, + NginxTemplateID: 1, + Type: "some-type", + }, + wantErr: "", + }, + { + name: "certificate does not exist", + host: host.Model{ + CertificateID: types.NullableDBUint{Uint: 9}, + }, + wantErr: "Certificate #9 does not exist: record not found", + }, + { + name: "upstream does not exist", + host: host.Model{ + UpstreamID: types.NullableDBUint{Uint: 9}, + }, + wantErr: "Upstream #9 does not exist: record not found", + }, + { + name: "proxy host and port set with upstream", + host: host.Model{ + UpstreamID: types.NullableDBUint{Uint: 1}, + ProxyHost: "proxy", + ProxyPort: 8080, + }, + wantErr: "Proxy Host or Port cannot be set when using an Upstream", + }, + { + name: "proxy host and port not set without upstream", + host: host.Model{ + ProxyHost: "", + ProxyPort: 0, + }, + wantErr: "Proxy Host and Port must be specified, unless using an Upstream", + }, + { + name: "nginx template does not exist", + host: host.Model{ + ProxyHost: "proxy", + ProxyPort: 8080, + NginxTemplateID: 9, + }, + wantErr: "Host Template #9 does not exist: record not found", + }, + { + name: "nginx template type mismatch", + host: host.Model{ + ProxyHost: "proxy", + ProxyPort: 8080, + NginxTemplateID: 8, + Type: "some-type", + }, + wantErr: "Host Template #8 is not valid for this host type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCert := new(MockCertificate) + mockUpstream := new(MockUpstream) + mockNginxTemplate := new(MockNginxTemplate) + + certificateGetByID = mockCert.GetByID + upstreamGetByID = mockUpstream.GetByID + nginxtemplateGetByID = mockNginxTemplate.GetByID + + // id 1 is valid + mockCert.On("GetByID", uint(1)).Return(certificate.Model{}, nil) + mockUpstream.On("GetByID", uint(1)).Return(upstream.Model{}, nil) + mockNginxTemplate.On("GetByID", uint(1)).Return(nginxtemplate.Model{Type: "some-type"}, nil) + + // id 9 is errors + mockCert.On("GetByID", uint(9)).Return(certificate.Model{}, gorm.ErrRecordNotFound) + mockUpstream.On("GetByID", uint(9)).Return(upstream.Model{}, gorm.ErrRecordNotFound) + mockNginxTemplate.On("GetByID", uint(9)).Return(nginxtemplate.Model{}, gorm.ErrRecordNotFound) + + // 8 is special + mockNginxTemplate.On("GetByID", uint(8)).Return(nginxtemplate.Model{Type: "different-type"}, nil) + + err := ValidateHost(tt.host) + if tt.wantErr != "" { + require.NotNil(t, err) + require.Equal(t, tt.wantErr, err.Error()) + } else { + require.Nil(t, err) + } + }) + } +} diff --git a/backend/internal/validator/upstreams.go b/backend/internal/validator/upstreams.go new file mode 100644 index 000000000..5d099f417 --- /dev/null +++ b/backend/internal/validator/upstreams.go @@ -0,0 +1,37 @@ +package validator + +import ( + "npm/internal/entity/upstream" + + "github.com/rotisserie/eris" +) + +// ValidateUpstream will check if associated objects exist and other checks +// will return a nil error if things are OK +func ValidateUpstream(u upstream.Model) error { + // Needs to have more than 1 server + if len(u.Servers) < 2 { + return eris.New("Upstreams require at least 2 servers") + } + + // Backup servers aren't permitted with hash balancing + if u.IPHash { + // check all servers for a backup param + for _, server := range u.Servers { + if server.Backup { + return eris.New("Backup servers cannot be used with hash balancing") + } + } + } + + // Check the nginx template exists and has the same type. + nginxTemplate, err := nginxtemplateGetByID(u.NginxTemplateID) + if err != nil { + return eris.Errorf("Nginx Template #%d does not exist", u.NginxTemplateID) + } + if nginxTemplate.Type != "upstream" { + return eris.Errorf("Host Template #%d is not valid for this upstream", u.NginxTemplateID) + } + + return nil +} diff --git a/backend/internal/validator/upstreams_test.go b/backend/internal/validator/upstreams_test.go new file mode 100644 index 000000000..e9b29a939 --- /dev/null +++ b/backend/internal/validator/upstreams_test.go @@ -0,0 +1,94 @@ +package validator + +import ( + "testing" + + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" + "npm/internal/entity/upstreamserver" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestValidateUpstream(t *testing.T) { + tests := []struct { + name string + upstreamModel upstream.Model + expectedError string + }{ + { + name: "less than 2 servers", + upstreamModel: upstream.Model{ + Servers: []upstreamserver.Model{ + {Server: "192.168.1.1"}, + }, + }, + expectedError: "Upstreams require at least 2 servers", + }, + { + name: "backup server with IP hash", + upstreamModel: upstream.Model{ + Servers: []upstreamserver.Model{ + {Server: "192.168.1.1", Backup: true}, + {Server: "192.168.1.2"}, + }, + IPHash: true, + }, + expectedError: "Backup servers cannot be used with hash balancing", + }, + { + name: "nginx template does not exist", + upstreamModel: upstream.Model{ + Servers: []upstreamserver.Model{ + {Server: "192.168.1.1"}, + {Server: "192.168.1.2"}, + }, + NginxTemplateID: 999, + }, + expectedError: "Nginx Template #999 does not exist", + }, + { + name: "nginx template type mismatch", + upstreamModel: upstream.Model{ + Servers: []upstreamserver.Model{ + {Server: "192.168.1.1"}, + {Server: "192.168.1.2"}, + }, + NginxTemplateID: 2, + }, + expectedError: "Host Template #2 is not valid for this upstream", + }, + { + name: "valid upstream", + upstreamModel: upstream.Model{ + Servers: []upstreamserver.Model{ + {Server: "192.168.1.1"}, + {Server: "192.168.1.2"}, + }, + NginxTemplateID: 1, + }, + expectedError: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockNginxTemplate := new(MockNginxTemplate) + nginxtemplateGetByID = mockNginxTemplate.GetByID + + mockNginxTemplate.On("GetByID", uint(1)).Return(nginxtemplate.Model{Type: "upstream"}, nil) + mockNginxTemplate.On("GetByID", uint(2)).Return(nginxtemplate.Model{Type: "redirect"}, nil) + mockNginxTemplate.On("GetByID", uint(999)).Return(nginxtemplate.Model{}, gorm.ErrRecordNotFound) + + err := ValidateUpstream(tt.upstreamModel) + if tt.expectedError != "" { + require.NotNil(t, err) + assert.Equal(t, tt.expectedError, err.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/backend/knexfile.js b/backend/knexfile.js deleted file mode 100644 index 391ca0050..000000000 --- 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 9d7329d94..000000000 --- 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 5a16a8642..000000000 --- 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 5a16a8642..000000000 --- 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 8f6dd8cc6..000000000 --- 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 8f6dd8cc6..000000000 --- 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 5a16a8642..000000000 --- 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 aeadc94ba..000000000 --- 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 bcdf66742..000000000 --- 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 bcdf66742..000000000 --- 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 9ccfa4f15..000000000 --- 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 9ccfa4f15..000000000 --- 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 bcdf66742..000000000 --- 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 a276c681d..000000000 --- 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 a276c681d..000000000 --- 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 87aa12e7d..000000000 --- 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 87aa12e7d..000000000 --- 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 a276c681d..000000000 --- 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 8480f9a1c..000000000 --- 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 166527a39..000000000 --- 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 166527a39..000000000 --- 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 d88e4cfff..000000000 --- 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 d88e4cfff..000000000 --- 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 166527a39..000000000 --- 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 342babc88..000000000 --- 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 342babc88..000000000 --- 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 ba2292064..000000000 --- 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 ba2292064..000000000 --- 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 342babc88..000000000 --- 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 dbc9e0c0f..000000000 --- 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 16b33b55b..000000000 --- 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 aeadc94ba..000000000 --- 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 aeadc94ba..000000000 --- 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 aeadc94ba..000000000 --- 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 fbeb1cc91..000000000 --- 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 fbeb1cc91..000000000 --- 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 7e9962874..000000000 --- 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 7e9962874..000000000 --- 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 fbeb1cc91..000000000 --- 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 aeadc94ba..000000000 --- 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 aeadc94ba..000000000 --- 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 2a2f0423a..000000000 --- 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 aeadc94ba..000000000 --- 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 aeadc94ba..000000000 --- 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 2a2f0423a..000000000 --- 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 aeadc94ba..000000000 --- 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 2a2f0423a..000000000 --- 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 9e456f051..000000000 --- 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 c9befeec8..000000000 --- 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 17edccec0..000000000 --- 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 44aa36934..000000000 --- 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 24ffa58d0..000000000 --- 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 4a37a4069..000000000 --- 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 e38be991e..000000000 --- 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 f75f77ef4..000000000 --- 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 4c8b62a84..000000000 --- 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 3f51b5969..000000000 --- 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 fca6f4bf2..000000000 --- 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 680af6d51..000000000 --- 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 263c87020..000000000 --- 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 a112e8261..000000000 --- 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 060548502..000000000 --- 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 05c277396..000000000 --- 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 9f6b43367..000000000 --- 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 22ae619ed..000000000 --- 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 2780c4df2..000000000 --- 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 4bcfd51ad..000000000 --- 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 648b162a0..000000000 --- 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 7dc9c1928..000000000 --- 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 3511e35b3..000000000 --- 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 ee0f0906f..000000000 --- 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/20201014143841_pass_auth.js b/backend/migrations/20201014143841_pass_auth.js deleted file mode 100644 index a7767eb19..000000000 --- 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 0dad48768..000000000 --- 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 b9bea0b92..000000000 --- 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/migrations/20210423103500_stream_domain.js b/backend/migrations/20210423103500_stream_domain.js deleted file mode 100644 index a894ca5e6..000000000 --- a/backend/migrations/20210423103500_stream_domain.js +++ /dev/null @@ -1,40 +0,0 @@ -const migrate_name = 'stream_domain'; -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('stream', (table) => { - table.renameColumn('forward_ip', 'forwarding_host'); - }) - .then(function () { - logger.info('[' + migrate_name + '] stream 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('stream', (table) => { - table.renameColumn('forwarding_host', 'forward_ip'); - }) - .then(function () { - logger.info('[' + migrate_name + '] stream Table altered'); - }); -}; diff --git a/backend/migrations/20211108145214_regenerate_default_host.js b/backend/migrations/20211108145214_regenerate_default_host.js deleted file mode 100644 index 4c50941ff..000000000 --- a/backend/migrations/20211108145214_regenerate_default_host.js +++ /dev/null @@ -1,50 +0,0 @@ -const migrate_name = 'stream_domain'; -const logger = require('../logger').migrate; -const internalNginx = require('../internal/nginx'); - -async function regenerateDefaultHost(knex) { - const row = await knex('setting').select('*').where('id', 'default-site').first(); - - if (!row) { - return Promise.resolve(); - } - - return internalNginx.deleteConfig('default') - .then(() => { - return internalNginx.generateConfig('default', row); - }) - .then(() => { - return internalNginx.test(); - }) - .then(() => { - return internalNginx.reload(); - }); -} - -/** - * Migrate - * - * @see http://knexjs.org/#Schema - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.up = function (knex) { - logger.info('[' + migrate_name + '] Migrating Up...'); - - return regenerateDefaultHost(knex); -}; - -/** - * Undo Migrate - * - * @param {Object} knex - * @param {Promise} Promise - * @returns {Promise} - */ -exports.down = function (knex) { - logger.info('[' + migrate_name + '] Migrating Down...'); - - return regenerateDefaultHost(knex); -}; \ No newline at end of file diff --git a/backend/models/access_list.js b/backend/models/access_list.js deleted file mode 100644 index 01974e86e..000000000 --- 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 932371f3e..000000000 --- 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 e257213a6..000000000 --- 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 a3a318c8f..000000000 --- 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 5ba5f3804..000000000 --- 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 6084a9953..000000000 --- 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 6de42a337..000000000 --- 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 def16d083..000000000 --- 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 a75830886..000000000 --- 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 dd149b769..000000000 --- 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 75aa90076..000000000 --- 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 ed65de0fc..000000000 --- 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 4e1b1826e..000000000 --- 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 c76f7dbf5..000000000 --- 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 bb87d5dc4..000000000 --- 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 3d6d13420..000000000 --- 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 28b6f178b..000000000 --- a/backend/package.json +++ /dev/null @@ -1,43 +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", - "archiver": "^5.3.0", - "batchflow": "^0.4.0", - "bcrypt": "^5.0.0", - "body-parser": "^1.19.0", - "compression": "^1.7.4", - "config": "^3.3.1", - "express": "^4.17.1", - "express-fileupload": "^1.1.9", - "gravatar": "^1.8.0", - "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.2.16", - "path": "^0.12.7", - "signale": "^1.4.0", - "sqlite3": "^4.1.1", - "temp-write": "^4.0.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 8a2490c3f..000000000 --- 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 33cbbc21f..000000000 --- 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 d55c3ae12..000000000 --- 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 ffdfb515d..000000000 --- a/backend/routes/api/nginx/certificates.js +++ /dev/null @@ -1,299 +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); - }); - -/** - * Test HTTP challenge for domains - * - * /api/nginx/certificates/test-http - */ -router - .route('/test-http') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - -/** - * GET /api/nginx/certificates/test-http - * - * Test HTTP challenge for domains - */ - .get((req, res, next) => { - internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains)) - .then((result) => { - res.status(200) - .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); - }); - -/** - * Download LE Certs - * - * /api/nginx/certificates/123/download - */ -router - .route('/:certificate_id/download') - .options((req, res) => { - res.sendStatus(204); - }) - .all(jwtdecode()) - - /** - * GET /api/nginx/certificates/123/download - * - * Renew certificate - */ - .get((req, res, next) => { - internalCertificate.download(res.locals.access, { - id: parseInt(req.params.certificate_id, 10) - }) - .then((result) => { - res.status(200) - .download(result.fileName); - }) - .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 08b58f2de..000000000 --- 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 6f933c3d3..000000000 --- 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 4d44c1126..000000000 --- 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 5e3fc28fe..000000000 --- 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 9e2c98c89..000000000 --- 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 fc6bd5bdf..000000000 --- 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 d08b2bf5c..000000000 --- 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 a21f998ae..000000000 --- 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 1c6bd0ad2..000000000 --- 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 4b4f3405c..000000000 --- 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": 6, - "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 404e32376..000000000 --- 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 955ca75c9..000000000 --- a/backend/schema/endpoints/certificates.json +++ /dev/null @@ -1,173 +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" - } - }, - { - "title": "Test HTTP Challenge", - "description": "Tests whether the HTTP challenge should work", - "href": "/nginx/certificates/{definitions.identity.example}/test-http", - "access": "private", - "method": "GET", - "rel": "info", - "http_header": { - "$ref": "../examples.json#/definitions/auth_header" - } - } - ] -} diff --git a/backend/schema/endpoints/dead-hosts.json b/backend/schema/endpoints/dead-hosts.json deleted file mode 100644 index 0c73c3be1..000000000 --- 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 9a3fff2fc..000000000 --- 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 14a469985..000000000 --- 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 29e2865ae..000000000 --- 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 159c8036e..000000000 --- a/backend/schema/endpoints/streams.json +++ /dev/null @@ -1,234 +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 - }, - "forwarding_host": { - "anyOf": [ - { - "$ref": "../definitions.json#/definitions/domain_name" - }, - { - "type": "string", - "format": "ipv4" - }, - { - "type": "string", - "format": "ipv6" - } - ] - }, - "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" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "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", - "forwarding_host", - "forwarding_port" - ], - "properties": { - "incoming_port": { - "$ref": "#/definitions/incoming_port" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "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" - }, - "forwarding_host": { - "$ref": "#/definitions/forwarding_host" - }, - "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 920af63f4..000000000 --- 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 42f44eac7..000000000 --- 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 37bc6c4d3..000000000 --- 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 6e7d1c8af..000000000 --- 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 000000000..44961b054 --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,22 @@ +#!/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 + +echo -e "${YELLOW}golangci-lint ...${RESET}" +if ! golangci-lint run -E goimports ./...; then + exit 1 +fi + +exit "$RESULT" diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh new file mode 100755 index 000000000..45ee9483e --- /dev/null +++ b/backend/scripts/test.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -eu + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if ! command -v go-test-coverage &>/dev/null; then + go install github.com/vladopajic/go-test-coverage/v2@latest +fi +if ! command -v tparse &>/dev/null; then + go install github.com/mfridman/tparse@latest +fi + +rm -f "$DIR/coverage.html" + +trap cleanup EXIT +cleanup() { + rm -f "$DIR/coverage.out" +} + +echo "Running go test suite ..." +go test -json -cover ./... -coverprofile="$DIR/coverage.out" | tparse +go tool cover -html="$DIR/coverage.out" -o "$DIR/coverage.html" +go-test-coverage -c "$DIR/.testcoverage.yml" diff --git a/backend/setup.js b/backend/setup.js deleted file mode 100644 index 47fd1e7b0..000000000 --- a/backend/setup.js +++ /dev/null @@ -1,233 +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.version_requirement || ''} ${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; - // Escape single quotes and backslashes - const escapedCredentials = certificate.meta.dns_provider_credentials.replaceAll('\'', '\\\'').replaceAll('\\', '\\\\'); - const credentials_cmd = '[ -f \'' + credentials_loc + '\' ] || { mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + escapedCredentials + '\' > \'' + 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(', ')); - }); - } - } - }); -}; - - -/** - * Starts a timer to call run the logrotation binary every two days - * @returns {Promise} - */ -const setupLogrotation = () => { - const intervalTimeout = 1000 * 60 * 60 * 24 * 2; // 2 days - - const runLogrotate = async () => { - try { - await utils.exec('logrotate /etc/logrotate.d/nginx-proxy-manager'); - logger.info('Logrotate completed.'); - } catch (e) { logger.warn(e); } - }; - - logger.info('Logrotate Timer initialized'); - setInterval(runLogrotate, intervalTimeout); - // And do this now as well - return runLogrotate(); -}; - -module.exports = function () { - return setupJwt() - .then(setupDefaultUser) - .then(setupDefaultSettings) - .then(setupCertbotPlugins) - .then(setupLogrotation); -}; diff --git a/backend/templates/_assets.conf b/backend/templates/_assets.conf deleted file mode 100644 index dcb183c55..000000000 --- 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 06ca7bb87..000000000 --- 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 002970d59..000000000 --- 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 7fade20ca..000000000 --- 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 8f996d34f..000000000 --- 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 11aecf24c..000000000 --- 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 730f3a7c4..000000000 --- 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 ssl{% if http2_support %} http2{% endif %}; -{% 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 5a7a6abeb..000000000 --- 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 d94dff57a..000000000 --- a/backend/templates/dead_host.conf +++ /dev/null @@ -1,23 +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 }}_access.log standard; - error_log /data/logs/dead-host-{{ id }}_error.log warn; - -{{ 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 ec68530ca..000000000 --- a/backend/templates/default.conf +++ /dev/null @@ -1,40 +0,0 @@ -# ------------------------------------------------------------ -# Default Site -# ------------------------------------------------------------ -{% if value == "congratulations" %} -# Skipping output, congratulations page configration is baked in. -{%- else %} -server { - listen 80 default; -{% if ipv6 -%} - listen [::]:80 default; -{% else -%} - #listen [::]:80 default; -{% endif %} - server_name default-host.localhost; - access_log /data/logs/default-host_access.log combined; - error_log /data/logs/default-host_error.log warn; -{% include "_exploits.conf" %} - - include conf.d/include/letsencrypt-acme-challenge.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 8ede2bd99..000000000 --- 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 676c8a60f..000000000 --- a/backend/templates/letsencrypt-request.conf +++ /dev/null @@ -1,19 +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_access.log standard; - error_log /data/logs/letsencrypt-requests_error.log warn; - - 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 ec30cca0d..000000000 --- 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 }}_access.log proxy; - error_log /data/logs/proxy-host-{{ id }}_error.log warn; - -{{ 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 339fe72ee..000000000 --- a/backend/templates/redirection_host.conf +++ /dev/null @@ -1,32 +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 }}_access.log standard; - error_log /data/logs/redirection-host-{{ id }}_error.log warn; - -{{ 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 76159a646..000000000 --- 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 {{ forwarding_host }}:{{ 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 {{ forwarding_host }}:{{ 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 968831827..000000000 --- a/backend/yarn.lock +++ /dev/null @@ -1,3754 +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, ajv@^6.12.6: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - -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== - -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - -archiver@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - -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== - -async@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" - integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== - -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= - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -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== - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -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-crc32@^0.2.1, buffer-crc32@^0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -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@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -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@^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.4: - 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== - -compress-commons@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" - integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.2" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - -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= - -crc-32@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" - integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" - -crc32-stream@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" - integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== - dependencies: - crc-32 "^1.2.0" - readable-stream "^3.4.0" - -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" - -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, end-of-stream@^1.4.1: - 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= - -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - -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-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-minipass@^1.2.7: - 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" - -glob@^7.1.4: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - 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== - -graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -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-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" - -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.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" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -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, 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" - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -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.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -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.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - -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= - -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.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.3.3: - 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.1, mkdirp@^0.5.3, mkdirp@^0.5.5: - 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" - -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.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -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.2.16: - version "2.2.16" - resolved "https://registry.yarnpkg.com/objection/-/objection-2.2.16.tgz#552ec6d625a7f80d6e204fc63732cbd3fc56f31c" - integrity sha512-sq8erZdxW5ruPUK6tVvwDxyO16U49XAn/BmOm2zaNhNA2phOPCe2/7+R70nDEF1SFrgJOrwDu/PtoxybuJxnjQ== - dependencies: - ajv "^6.12.6" - 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" - -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.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -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@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== - -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= - -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== - -printj@~1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" - integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== - -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@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.0, readable-stream@^2.0.5, 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" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdir-glob@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" - integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== - dependencies: - minimatch "^3.0.4" - -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" - -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, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - 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@^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@^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" - -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.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.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-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-stream@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^4, tar@^4.4.2: - version "4.4.19" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" - integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== - dependencies: - chownr "^1.1.4" - fs-minipass "^1.2.7" - minipass "^2.9.0" - minizlib "^1.3.3" - mkdirp "^0.5.5" - safe-buffer "^5.2.1" - yallist "^3.1.1" - -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.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" - -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, 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== - -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.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -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" - -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0" diff --git a/docker/Dockerfile b/docker/Dockerfile index 378fffbfc..a7002fa5a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,63 +1,104 @@ # This is a Dockerfile intended to be built using `docker buildx` # for multi-arch support. Building with `docker build` may have unexpected results. -# This file assumes that the frontend has been built using ./scripts/frontend-build +# This file assumes that these scripts have been run first: +# - ./scripts/ci/build-frontend -FROM nginxproxymanager/nginx-full:certbot-node +FROM nginxproxymanager/testca AS testca +FROM ghcr.io/letsencrypt/pebble AS pebbleca +FROM jc21/gotools:latest AS gobuild + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] -ARG TARGETPLATFORM -ARG BUILD_VERSION ARG BUILD_COMMIT -ARG BUILD_DATE +ARG BUILD_VERSION +ARG GOPRIVATE +ARG GOPROXY -ENV SUPPRESS_NO_CONFIG_WARNING=1 \ - S6_FIX_ATTRS_HIDDEN=1 \ +ENV BUILD_COMMIT="${BUILD_COMMIT:-dev}" \ + BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \ + CGO_ENABLED=0 \ + GOPRIVATE="${GOPRIVATE:-}" \ + GOPROXY="${GOPROXY:-}" + +COPY scripts /scripts +COPY backend /app +WORKDIR /app + +ARG ARG TARGETPLATFORM +RUN mkdir -p /dist \ + && /scripts/go-multiarch-wrapper /dist/server /dist/ipranges + +#=============== +# Final image +#=============== + +FROM nginxproxymanager/nginx-full:acmesh AS final + +COPY --from=gobuild /dist/server /app/bin/server +COPY --from=gobuild /dist/ipranges /app/bin/ipranges +# these certs are used for testing in CI +COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt + +# These acmesh vars are defined in the base image +ENV ACMESH_CONFIG_HOME=/data/.acme.sh/config \ + ACMESH_HOME=/data/.acme.sh \ + CERT_HOME=/data/.acme.sh/certs \ + LE_CONFIG_HOME=/data/.acme.sh/config \ + LE_WORKING_DIR=/data/.acme.sh \ S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ - NODE_ENV=production \ - NPM_BUILD_VERSION="${BUILD_VERSION}" \ - NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ - NPM_BUILD_DATE="${BUILD_DATE}" - -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ - && apt-get update \ - && apt-get install -y --no-install-recommends jq logrotate \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_FIX_ATTRS_HIDDEN=1 \ + S6_KILL_FINISH_MAXTIME=10000 \ + S6_VERBOSITY=1 + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf + +# fail2ban +RUN apt-get update \ + && apt-get install -y --no-install-recommends fail2ban logrotate \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* /etc/fail2ban # s6 overlay +ARG TARGETPLATFORM COPY scripts/install-s6 /tmp/install-s6 -RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 - -EXPOSE 80 81 443 +RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -rf /tmp/* -COPY backend /app -COPY frontend/dist /app/frontend -COPY global /app/global +EXPOSE 80/tcp 81/tcp 443/tcp -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 +# and remove any other cruft +RUN rm -rf /etc/services.d/frontend \ + /etc/nginx/conf.d/dev.conf \ + /var/cache/* \ + /var/log/* \ + /tmp/* \ + /var/lib/dpkg/status-old + +VOLUME /data -# Change permission of logrotate config file -RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager +CMD [ "/init" ] -# fix for pip installs -# https://github.com/NginxProxyManager/nginx-proxy-manager/issues/1769 -RUN pip uninstall --yes setuptools \ - && pip install "setuptools==58.0.0" +ARG NOW +ARG BUILD_VERSION +ARG BUILD_COMMIT +ARG BUILD_DATE -VOLUME [ "/data", "/etc/letsencrypt" ] -ENTRYPOINT [ "/init" ] +ENV NPM_BUILD_VERSION="${BUILD_VERSION:-0.0.0}" \ + NPM_BUILD_COMMIT="${BUILD_COMMIT:-dev}" \ + NPM_BUILD_DATE="${BUILD_DATE:-}" 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.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.description="Nginx Host Management and Proxy" \ + org.label-schema.build-date="${NOW:-}" \ + org.label-schema.version="${BUILD_VERSION:-0.0.0}" \ + org.label-schema.url="https://nginxproxymanager.com" \ + org.label-schema.vcs-url="https://github.com/NginxProxyManager/nginx-proxy-manager.git" \ + org.label-schema.vcs-ref="${BUILD_COMMIT:-dev}" \ + org.label-schema.cmd="docker run --rm -ti jc21/nginx-proxy-manager:${BUILD_VERSION:-0.0.0}" diff --git a/docker/ci.env b/docker/ci.env new file mode 100644 index 000000000..7128295dd --- /dev/null +++ b/docker/ci.env @@ -0,0 +1,8 @@ +AUTHENTIK_SECRET_KEY=gl8woZe8L6IIX8SC0c5Ocsj0xPkX5uJo5DVZCFl+L/QGbzuplfutYuua2ODNLEiDD3aFd9H2ylJmrke0 +AUTHENTIK_REDIS__HOST=authentik-redis +AUTHENTIK_POSTGRESQL__HOST=db-postgres +AUTHENTIK_POSTGRESQL__USER=authentik +AUTHENTIK_POSTGRESQL__NAME=authentik +AUTHENTIK_POSTGRESQL__PASSWORD=07EKS5NLI6Tpv68tbdvrxfvj +AUTHENTIK_BOOTSTRAP_PASSWORD=admin +AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com diff --git a/docker/ci/postgres/authentik.sql.gz b/docker/ci/postgres/authentik.sql.gz new file mode 100644 index 000000000..49665d4e6 Binary files /dev/null and b/docker/ci/postgres/authentik.sql.gz differ diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index d2e2266a1..a59c636a1 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,29 +1,55 @@ -FROM nginxproxymanager/nginx-full:certbot-node +FROM nginxproxymanager/testca AS testca +FROM ghcr.io/letsencrypt/pebble AS pebbleca +FROM nginxproxymanager/nginx-full:acmesh-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 ACMESH_CONFIG_HOME=/data/.acme.sh/config \ + ACMESH_HOME=/data/.acme.sh \ + CERT_HOME=/data/.acme.sh/certs \ + CGO_ENABLED=0 \ + GOPROXY=$GOPROXY \ + GOPRIVATE=$GOPRIVATE \ + LE_CONFIG_HOME=/data/.acme.sh/config \ + LE_WORKING_DIR=/data/.acme.sh \ + S6_BEHAVIOUR_IF_STAGE2_FAILS=1 \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_FIX_ATTRS_HIDDEN=1 \ + S6_KILL_FINISH_MAXTIME=10000 \ + S6_VERBOSITY=2 + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf + +# node, fail2ban +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get update \ - && apt-get install -y certbot jq python3-pip logrotate \ + && apt-get install -y --no-install-recommends nodejs vim dnsutils fail2ban logrotate \ + && npm install --location=global yarn \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* /etc/fail2ban # Task RUN cd /usr \ && curl -sL https://taskfile.dev/install.sh | sh \ && cd /root -COPY rootfs / +COPY docker/rootfs / RUN rm -f /etc/nginx/conf.d/production.conf -RUN chmod 644 /etc/logrotate.d/nginx-proxy-manager # s6 overlay -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 / +COPY scripts/install-s6 /tmp/install-s6 +RUN /tmp/install-s6 && rm -rf /tmp/* + +# Fix for golang dev: +RUN chown -R 1000:1000 /opt/go -EXPOSE 80 81 443 -ENTRYPOINT [ "/init" ] +COPY --from=pebbleca /test/certs/pebble.minica.pem /etc/ssl/certs/pebble.minica.pem +COPY --from=testca /home/step/certs/root_ca.crt /etc/ssl/certs/NginxProxyManager.crt +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/dev/dnsrouter-config.json b/docker/dev/dnsrouter-config.json new file mode 100644 index 000000000..fbeab98e9 --- /dev/null +++ b/docker/dev/dnsrouter-config.json @@ -0,0 +1,31 @@ +{ + "log": { + "format": "nice", + "level": "debug" + }, + "servers": [ + { + "host": "0.0.0.0", + "port": 53, + "upstreams": [ + { + "regex": "website[0-9]+.example\\.com", + "upstream": "127.0.0.11" + }, + { + "regex": ".*\\.example\\.com", + "upstream": "1.1.1.1" + }, + { + "regex": "local", + "nxdomain": true + } + ], + "internal": null, + "default_upstream": "127.0.0.11" + } + ], + "cache": { + "disabled": true + } +} diff --git a/docker/dev/pdns-db.sql b/docker/dev/pdns-db.sql new file mode 100644 index 000000000..c182cf785 --- /dev/null +++ b/docker/dev/pdns-db.sql @@ -0,0 +1,255 @@ +/* + +How this was generated: +1. bring up an empty pdns stack +2. use api to create a zone ... + +curl -X POST \ + 'http://npm.dev:8081/api/v1/servers/localhost/zones' \ + --header 'X-API-Key: npm' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "name": "example.com.", + "kind": "Native", + "masters": [], + "nameservers": [ + "ns1.pdns.", + "ns2.pdns." + ] +}' + +3. Dump sql: + +docker exec -ti npm.pdns.db mysqldump -u pdns -p pdns + +*/ + +---------------------------------------------------------------------- + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `comments` +-- + +DROP TABLE IF EXISTS `comments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `comments` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `name` varchar(255) NOT NULL, + `type` varchar(10) NOT NULL, + `modified_at` int(11) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + `comment` text CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`id`), + KEY `comments_name_type_idx` (`name`,`type`), + KEY `comments_order_idx` (`domain_id`,`modified_at`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `comments` +-- + +LOCK TABLES `comments` WRITE; +/*!40000 ALTER TABLE `comments` DISABLE KEYS */; +/*!40000 ALTER TABLE `comments` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cryptokeys` +-- + +DROP TABLE IF EXISTS `cryptokeys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cryptokeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `flags` int(11) NOT NULL, + `active` tinyint(1) DEFAULT NULL, + `published` tinyint(1) DEFAULT 1, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainidindex` (`domain_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cryptokeys` +-- + +LOCK TABLES `cryptokeys` WRITE; +/*!40000 ALTER TABLE `cryptokeys` DISABLE KEYS */; +/*!40000 ALTER TABLE `cryptokeys` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `domainmetadata` +-- + +DROP TABLE IF EXISTS `domainmetadata`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `domainmetadata` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) NOT NULL, + `kind` varchar(32) DEFAULT NULL, + `content` text DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `domainmetadata_idx` (`domain_id`,`kind`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `domainmetadata` +-- + +LOCK TABLES `domainmetadata` WRITE; +/*!40000 ALTER TABLE `domainmetadata` DISABLE KEYS */; +INSERT INTO `domainmetadata` VALUES +(1,1,'SOA-EDIT-API','DEFAULT'); +/*!40000 ALTER TABLE `domainmetadata` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `domains` +-- + +DROP TABLE IF EXISTS `domains`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `domains` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `master` varchar(128) DEFAULT NULL, + `last_check` int(11) DEFAULT NULL, + `type` varchar(8) NOT NULL, + `notified_serial` int(10) unsigned DEFAULT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 DEFAULT NULL, + `options` varchar(64000) DEFAULT NULL, + `catalog` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name_index` (`name`), + KEY `catalog_idx` (`catalog`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `domains` +-- + +LOCK TABLES `domains` WRITE; +/*!40000 ALTER TABLE `domains` DISABLE KEYS */; +INSERT INTO `domains` VALUES +(1,'example.com','',NULL,'NATIVE',NULL,'',NULL,NULL); +/*!40000 ALTER TABLE `domains` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `records` +-- + +DROP TABLE IF EXISTS `records`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `records` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `domain_id` int(11) DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `type` varchar(10) DEFAULT NULL, + `content` varchar(64000) DEFAULT NULL, + `ttl` int(11) DEFAULT NULL, + `prio` int(11) DEFAULT NULL, + `disabled` tinyint(1) DEFAULT 0, + `ordername` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, + `auth` tinyint(1) DEFAULT 1, + PRIMARY KEY (`id`), + KEY `nametype_index` (`name`,`type`), + KEY `domain_id` (`domain_id`), + KEY `ordername` (`ordername`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `records` +-- + +LOCK TABLES `records` WRITE; +/*!40000 ALTER TABLE `records` DISABLE KEYS */; +INSERT INTO `records` VALUES +(1,1,'example.com','NS','ns1.pdns',1500,0,0,NULL,1), +(2,1,'example.com','NS','ns2.pdns',1500,0,0,NULL,1), +(3,1,'example.com','SOA','a.misconfigured.dns.server.invalid hostmaster.example.com 2023030501 10800 3600 604800 3600',1500,0,0,NULL,1); +/*!40000 ALTER TABLE `records` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `supermasters` +-- + +DROP TABLE IF EXISTS `supermasters`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `supermasters` ( + `ip` varchar(64) NOT NULL, + `nameserver` varchar(255) NOT NULL, + `account` varchar(40) CHARACTER SET utf8mb3 NOT NULL, + PRIMARY KEY (`ip`,`nameserver`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `supermasters` +-- + +LOCK TABLES `supermasters` WRITE; +/*!40000 ALTER TABLE `supermasters` DISABLE KEYS */; +/*!40000 ALTER TABLE `supermasters` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `tsigkeys` +-- + +DROP TABLE IF EXISTS `tsigkeys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `tsigkeys` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) DEFAULT NULL, + `algorithm` varchar(50) DEFAULT NULL, + `secret` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `namealgoindex` (`name`,`algorithm`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `tsigkeys` +-- + +LOCK TABLES `tsigkeys` WRITE; +/*!40000 ALTER TABLE `tsigkeys` DISABLE KEYS */; +/*!40000 ALTER TABLE `tsigkeys` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; diff --git a/docker/dev/pebble-config.json b/docker/dev/pebble-config.json new file mode 100644 index 000000000..289d29069 --- /dev/null +++ b/docker/dev/pebble-config.json @@ -0,0 +1,12 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:443", + "managementListenAddress": "0.0.0.0:15000", + "certificate": "test/certs/localhost/cert.pem", + "privateKey": "test/certs/localhost/key.pem", + "httpPort": 80, + "tlsPort": 443, + "ocspResponderURL": "", + "externalAccountBindingRequired": false + } +} \ No newline at end of file diff --git a/docker/docker-compose.ci.mysql.yml b/docker/docker-compose.ci.mysql.yml new file mode 100644 index 000000000..06ecdaa37 --- /dev/null +++ b/docker/docker-compose.ci.mysql.yml @@ -0,0 +1,30 @@ +# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. +services: + + cypress: + environment: + CYPRESS_stack: 'mysql' + + fullstack: + environment: + NPM_DB_DRIVER: 'mysql' + NPM_DB_HOST: 'db-mysql' + NPM_DB_PORT: '3306' + NPM_DB_USERNAME: 'npm' + NPM_DB_PASSWORD: 'npmpass' + NPM_DB_NAME: 'npm' + depends_on: + - db-mysql + + db-mysql: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npmpass' + volumes: + - mysql_vol:/var/lib/mysql + +volumes: + mysql_vol: diff --git a/docker/docker-compose.ci.postgres.yml b/docker/docker-compose.ci.postgres.yml new file mode 100644 index 000000000..80f06d3ea --- /dev/null +++ b/docker/docker-compose.ci.postgres.yml @@ -0,0 +1,78 @@ +# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. +services: + + cypress: + environment: + CYPRESS_stack: 'postgres' + + fullstack: + environment: + NPM_DB_DRIVER: 'postgres' + NPM_DB_HOST: 'db-postgres' + NPM_DB_PORT: '5432' + NPM_DB_USERNAME: 'npm' + NPM_DB_PASSWORD: 'npmpass' + NPM_DB_NAME: 'npm' + NPM_DB_SSLMODE: 'disable' + depends_on: + - db-postgres + - authentik + - authentik-worker + - authentik-ldap + + db-postgres: + image: postgres:latest + environment: + POSTGRES_USER: 'npm' + POSTGRES_PASSWORD: 'npmpass' + POSTGRES_DB: 'npm' + volumes: + - psql_vol:/var/lib/postgresql/data + - ./ci/postgres:/docker-entrypoint-initdb.d + + authentik-redis: + image: 'redis:alpine' + command: --save 60 1 --loglevel warning + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli ping | grep PONG'] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis_vol:/data + + authentik: + image: ghcr.io/goauthentik/server:2024.10.1 + restart: unless-stopped + command: server + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + + authentik-worker: + image: ghcr.io/goauthentik/server:2024.10.1 + restart: unless-stopped + command: worker + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + + authentik-ldap: + image: ghcr.io/goauthentik/ldap:2024.10.1 + environment: + AUTHENTIK_HOST: 'http://authentik:9000' + AUTHENTIK_INSECURE: 'true' + AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp' + restart: unless-stopped + depends_on: + - authentik + +volumes: + psql_vol: + redis_vol: diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index a8049ec81..21a343a9b 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -1,80 +1,96 @@ -# WARNING: This is a CI docker-compose file used for building and testing of the entire app, it should not be used for production. -version: "3" +# WARNING: This is a CI docker-compose file used for building +# and testing of the entire app, it should not be used for production. services: - fullstack-mysql: - image: ${IMAGE}:ci-${BUILD_NUMBER} + fullstack: + image: "${IMAGE}:${BRANCH_LOWER}-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" + NPM_DB_DRIVER: 'sqlite' + NPM_LOG_LEVEL: 'debug' + NPM_LOG_FORMAT: 'json' + NPM_DISABLE_IPV6: 'true' volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 - depends_on: - - db - healthcheck: - test: ["CMD", "/bin/check-health"] - interval: 10s - timeout: 3s + - '/etc/localtime:/etc/localtime:ro' + - 'npm_data_ci:/data' + - '../docs:/temp-docs' + - './dev/resolv.conf:/etc/resolv.conf:ro' + networks: + default: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com - fullstack-sqlite: - image: ${IMAGE}:ci-${BUILD_NUMBER} - environment: - NODE_ENV: "development" - FORCE_COLOR: 1 - DB_SQLITE_FILE: "/data/database.sqlite" + stepca: + image: jc21/testca volumes: - - npm_data:/data - expose: - - 81 - - 80 - - 443 - healthcheck: - test: ["CMD", "/bin/check-health"] - interval: 10s - timeout: 3s + - ./dev/resolv.conf:/etc/resolv.conf:ro + - '/etc/localtime:/etc/localtime:ro' + networks: + default: + aliases: + - ca.internal - db: - image: jc21/mariadb-aria - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" + pdns: + image: pschiffe/pdns-mysql:4.8 volumes: - - db_data:/var/lib/mysql + - '/etc/localtime:/etc/localtime:ro' + environment: + PDNS_master: 'yes' + PDNS_api: 'yes' + PDNS_api_key: 'npm' + PDNS_webserver: 'yes' + PDNS_webserver_address: '0.0.0.0' + PDNS_webserver_password: 'npm' + PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_version_string: 'anonymous' + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_gmysql_host: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + default: + aliases: + - ns1.pdns + - ns2.pdns - cypress-mysql: - image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} - build: - context: ../test/ - dockerfile: cypress/Dockerfile + pdns-db: + image: mariadb environment: - CYPRESS_baseUrl: "http://fullstack-mysql:81" + MYSQL_ROOT_PASSWORD: 'pdns' + MYSQL_DATABASE: 'pdns' + MYSQL_USER: 'pdns' + MYSQL_PASSWORD: 'pdns' + volumes: + - 'pdns_mysql_vol:/var/lib/mysql' + - '/etc/localtime:/etc/localtime:ro' + - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro' + + dnsrouter: + image: jc21/dnsrouter volumes: - - cypress-logs:/results - command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + entrypoint: /dnsrouter -v -c /dnsrouter-config.json - cypress-sqlite: - image: ${IMAGE}-cypress:ci-${BUILD_NUMBER} + cypress: + image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}" build: - context: ../test/ - dockerfile: cypress/Dockerfile + context: ../ + dockerfile: test/cypress/Dockerfile environment: - CYPRESS_baseUrl: "http://fullstack-sqlite:81" + CYPRESS_baseUrl: 'http://fullstack:81' + CYPRESS_stack: 'sqlite' volumes: - - cypress-logs:/results - command: cypress run --browser chrome --config-file=${CYPRESS_CONFIG:-cypress/config/ci.json} + - 'cypress_logs:/results' + - './dev/resolv.conf:/etc/resolv.conf:ro' + command: cypress run --browser chrome --config-file=cypress/config/ci.js volumes: - cypress-logs: - npm_data: - db_data: + cypress_logs: + npm_data_ci: + pdns_mysql_vol: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 79fbd7999..d1ed520b2 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,62 +1,127 @@ -# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. -version: "3.5" +# WARNING: This is a DEVELOPMENT docker-compose file used for development of the entire app, it should not be used for production. services: + npm: image: nginxproxymanager:dev - container_name: npm_core + container_name: 'npm.dev' build: - context: ./ - dockerfile: ./dev/Dockerfile + context: ../ + dockerfile: ./docker/dev/Dockerfile + args: + GOPROXY: "${GOPROXY:-}" + GOPRIVATE: "${GOPRIVATE:-}" ports: - 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" + #DEBUG: 'true' + DEVELOPMENT: 'true' + GOPROXY: "${GOPROXY:-}" + GOPRIVATE: "${GOPRIVATE:-}" + YARN_REGISTRY: "${YARN_REGISTRY:-}" + NPM_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 + - /etc/localtime:/etc/localtime:ro + - ../:/app + - ./rootfs/var/www/html:/var/www/html + - ./dev/resolv.conf:/etc/resolv.conf:ro + - npm_data_vol:/data working_dir: /app + networks: + default: + aliases: + - website1.internal + - website2.internal + - website3.internal + restart: unless-stopped + + npm-stepca: + image: jc21/testca + container_name: "npm.stepca" + volumes: + - ./dev/resolv.conf:/etc/resolv.conf:ro + networks: + default: + aliases: + - ca.internal + + npm-pebble: + image: letsencrypt/pebble + container_name: "npm.pebble" + command: pebble -config /test/config/pebble-config.json + environment: + PEBBLE_VA_SLEEPTIME: 2 + volumes: + - ./dev/pebble-config.json:/test/config/pebble-config.json + - ./dev/resolv.conf:/etc/resolv.conf:ro + networks: + default: + aliases: + # required for https cert dns san + - pebble - db: - image: jc21/mariadb-aria - container_name: npm_db + npm-swagger: + image: swaggerapi/swagger-ui:latest + container_name: "npm.swagger" ports: - - 33306:3306 + - 3001:80 + environment: + URL: "http://${SWAGGER_PUBLIC_DOMAIN:-127.0.0.1:3081}/api/schema" + PORT: '80' + depends_on: + - npm + + npm-pdns: + image: pschiffe/pdns-mysql:4.8 + container_name: "npm.pdns" + volumes: + - '/etc/localtime:/etc/localtime:ro' + environment: + PDNS_master: 'yes' + PDNS_api: 'yes' + PDNS_api_key: 'npm' + PDNS_webserver: 'yes' + PDNS_webserver_address: '0.0.0.0' + PDNS_webserver_password: 'npm' + PDNS_webserver-allow-from: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_version_string: 'anonymous' + PDNS_default_ttl: 1500 + PDNS_allow_axfr_ips: '127.0.0.0/8,192.0.0.0/8,10.0.0.0/8,172.0.0.0/8' + PDNS_gmysql_host: npm-pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - npm-pdns-db networks: - - nginx_proxy_manager + default: + aliases: + - ns1.pdns + - ns2.pdns + + npm-pdns-db: + image: mariadb:10.7.1 + container_name: "npm.pdns.db" environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" + MYSQL_ROOT_PASSWORD: 'pdns' + MYSQL_DATABASE: 'pdns' + MYSQL_USER: 'pdns' + MYSQL_PASSWORD: 'pdns' + volumes: + - npm_pdns_mysql_vol:/var/lib/mysql + - /etc/localtime:/etc/localtime:ro + - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro' + + npm-dnsrouter: + image: jc21/dnsrouter + container_name: "npm.dnsrouter" volumes: - - db_data:/var/lib/mysql + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro volumes: - npm_data: - name: npm_core_data - le_data: - name: npm_le_data - db_data: - name: npm_db_data - -networks: - nginx_proxy_manager: - name: npm_network + npm_data_vol: + npm_pdns_mysql_vol: diff --git a/docker/rootfs/bin/check-health b/docker/rootfs/bin/check-health deleted file mode 100755 index bcf5552b3..000000000 --- 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 2aa0e41a9..000000000 --- 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-finish.d/.gitignore b/docker/rootfs/etc/cont-finish.d/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/docker/rootfs/etc/cont-finish.d/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/docker/rootfs/etc/cont-init.d/.gitignore b/docker/rootfs/etc/cont-init.d/.gitignore deleted file mode 100644 index f04f0f6e0..000000000 --- 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_perms.sh b/docker/rootfs/etc/cont-init.d/01_perms.sh deleted file mode 100755 index e7875d329..000000000 --- a/docker/rootfs/etc/cont-init.d/01_perms.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/with-contenv bash -set -e - -mkdir -p /data/logs -echo "Changing ownership of /data/logs to $(id -u):$(id -g)" -chown -R "$(id -u):$(id -g)" /data/logs - 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 f145807ab..000000000 --- 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/fix-attrs.d/.gitignore b/docker/rootfs/etc/fix-attrs.d/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/docker/rootfs/etc/fix-attrs.d/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/docker/rootfs/etc/letsencrypt.ini b/docker/rootfs/etc/letsencrypt.ini deleted file mode 100644 index aae53b902..000000000 --- a/docker/rootfs/etc/letsencrypt.ini +++ /dev/null @@ -1,6 +0,0 @@ -text = True -non-interactive = True -webroot-path = /data/letsencrypt-acme-challenge -key-type = ecdsa -elliptic-curve = secp384r1 -preferred-chain = ISRG Root X1 diff --git a/docker/rootfs/etc/nginx/conf.d/default.conf b/docker/rootfs/etc/nginx/conf.d/default.conf index 37d316db5..4d7007339 100644 --- a/docker/rootfs/etc/nginx/conf.d/default.conf +++ b/docker/rootfs/etc/nginx/conf.d/default.conf @@ -1,18 +1,10 @@ # "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/fallback_access.log standard; - error_log /data/logs/fallback_error.log warn; - include conf.d/include/assets.conf; + listen 80 default; + server_name localhost; + include conf.d/include/acme-challenge.conf; include conf.d/include/block-exploits.conf; - include conf.d/include/letsencrypt-acme-challenge.conf; + access_log /data/logs/default.log proxy; location / { index index.html; @@ -22,19 +14,11 @@ 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"; - + listen 443 ssl default; server_name localhost; - access_log /data/logs/fallback_access.log standard; - error_log /dev/null crit; - ssl_certificate /data/nginx/dummycert.pem; - ssl_certificate_key /data/nginx/dummykey.pem; include conf.d/include/ssl-ciphers.conf; - + include conf.d/include/block-exploits.conf; + access_log /data/logs/default.log proxy; + ssl_reject_handshake on; return 444; } diff --git a/docker/rootfs/etc/nginx/conf.d/dev.conf b/docker/rootfs/etc/nginx/conf.d/dev.conf index edbdec8ac..34d0340da 100644 --- a/docker/rootfs/etc/nginx/conf.d/dev.conf +++ b/docker/rootfs/etc/nginx/conf.d/dev.conf @@ -1,29 +1,62 @@ server { listen 81 default; - listen [::]:81 default; - server_name nginxproxymanager-dev; - root /app/frontend/dist; - access_log /dev/null; location /api { return 302 /api/; } + root /app/backend; + location /api/coverage { + try_files /index.html /coverage.html; + } + + # go server location /api/ { + add_header X-Served-By $host; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ''; + 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-Accel-Buffering no; + proxy_pass http://127.0.0.1:3000/api/; + } + + # go server + location /oauth/ { + add_header X-Served-By $host; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ''; + 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-Accel-Buffering no; + proxy_pass http://127.0.0.1:3000/oauth/; + } + + location ~ .html { + try_files $uri =404; + } + + # vite dev server + location / { 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:3000/; - - proxy_read_timeout 15m; - proxy_send_timeout 15m; - } - - location / { - index index.html; - try_files $uri $uri.html $uri/ /index.html; + proxy_pass http://127.0.0.1:5173; } } 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 5291fe15e..000000000 --- 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/acme-challenge.conf b/docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf new file mode 100644 index 000000000..db408c777 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/acme-challenge.conf @@ -0,0 +1,17 @@ +# 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/ { + auth_basic off; + auth_request off; + allow all; + default_type "text/plain"; + root "/data/.acme.sh/.well-known"; +} + +# 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/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf index e95c2e8b7..7dd0f5cea 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 093bda235..22360fc1a 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 15f0d2856..5fd4810f8 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 342493254..000000000 --- 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 ff2a78274..000000000 --- a/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf +++ /dev/null @@ -1,30 +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; - auth_request 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 fcaaf0038..b84a45135 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$request_uri; - +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 000000000..ccd9dcef9 --- /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 877e51dda..2881604f9 100644 --- a/docker/rootfs/etc/nginx/conf.d/production.conf +++ b/docker/rootfs/etc/nginx/conf.d/production.conf @@ -1,33 +1,20 @@ # 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/ { - 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; + add_header X-Served-By $host; + chunked_transfer_encoding off; + proxy_buffering off; + proxy_cache off; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header Connection ''; + 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-Accel-Buffering no; + proxy_pass http://localhost:3000/; } } diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 4d5ee9017..3cafede3b 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -1,7 +1,7 @@ # run nginx in foreground daemon off; - -user root; +pid /run/nginx/nginx.pid; +user npm; # Set number of worker processes automatically based on number of CPU cores. worker_processes auto; @@ -26,15 +26,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,7 +40,7 @@ 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 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"'; 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"'; access_log /data/logs/fallback_access.log proxy; @@ -57,13 +54,13 @@ 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; # NPM generated CDN ip ranges: - include conf.d/include/ip_ranges.conf; + include conf.d/include/ipranges.conf; # always put the following 2 lines after ip subnets: real_ip_header X-Real-IP; real_ip_recursive on; @@ -74,10 +71,9 @@ http { # 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; + include /data/nginx/upstreams/*.conf; + include /data/nginx/hosts/*.conf; + include /data/nginx/streams/*.conf; # Custom include /data/nginx/custom/http[.]conf; diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/dependencies.d/prepare new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run new file mode 100755 index 000000000..c90b7219b --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run @@ -0,0 +1,26 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /bin/common.sh + +if [ "$(is_true "$DEBUG")" = '1' ]; then + set -x +fi + +log_info 'Starting backend ...' + +if [ "$(is_true "$DEVELOPMENT")" = '1' ]; then + HOME=$NPMHOME + GOPATH="$HOME/go" + mkdir -p "$GOPATH" + chown -R "$PUID:$PGID" "$GOPATH" + export HOME GOPATH + rm -rf /app/backend/.task + cd /app/backend || exit 1 + exec s6-setuidgid "$PUID:$PGID" task -w +else + cd /app/bin || exit 1 + exec s6-setuidgid "$PUID:$PGID" /app/bin/server +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/dependencies.d/prepare new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/run new file mode 100755 index 000000000..adf64ce27 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/run @@ -0,0 +1,11 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +. /bin/common.sh + +if [ "$(is_true "$DEBUG")" = '1' ]; then + set -x +fi + +log_info 'Starting fail2ban ...' +exec /usr/bin/fail2ban-client -c /fail2ban -x -vv -f start diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/fail2ban/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/dependencies.d/prepare new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run new file mode 100755 index 000000000..4f92a9e9f --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run @@ -0,0 +1,26 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /bin/common.sh + +if [ "$(is_true "$DEBUG")" = '1' ]; then + set -x +fi + +# This service is DEVELOPMENT only. +if [ "$(is_true "$DEVELOPMENT")" = '1' ]; then + CI=true + HOME=$NPMHOME + export CI + export HOME + + cd /app/frontend || exit 1 + + log_info 'Starting frontend ...' + s6-setuidgid "$PUID:$PGID" yarn install + exec s6-setuidgid "$PUID:$PGID" yarn dev +else + exit 0 +fi diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/dependencies.d/prepare new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run new file mode 100755 index 000000000..1f81e4cbb --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -0,0 +1,13 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /bin/common.sh + +if [ "$(is_true "$DEBUG")" = '1' ]; then + set -x +fi + +log_info 'Starting nginx ...' +exec s6-setuidgid "$PUID:$PGID" nginx diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/type @@ -0,0 +1 @@ +longrun diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh new file mode 100755 index 000000000..0ce831d0e --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/00-all.sh @@ -0,0 +1,22 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /bin/common.sh + +if [ "$(id -u)" != "0" ]; then + log_fatal "This docker container must be run as root, do not specify a user.\nYou can specify PUID and PGID env vars to run processes as that user and group after initialization." +fi + +if [ "$(is_true "$DEBUG")" = '1' ]; then + set -x +fi + +. /etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh +. /etc/s6-overlay/s6-rc.d/prepare/20-paths.sh +. /etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh +. /etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh +. /etc/s6-overlay/s6-rc.d/prepare/50-ipv46.sh +. /etc/s6-overlay/s6-rc.d/prepare/60-fail2ban.sh +. /etc/s6-overlay/s6-rc.d/prepare/90-banner.sh diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh new file mode 100755 index 000000000..ea1001938 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/10-usergroup.sh @@ -0,0 +1,40 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info "Configuring $NPMUSER user ..." + +if id -u "$NPMUSER" 2>/dev/null; then + # user already exists + usermod -u "$PUID" "$NPMUSER" +else + # Add user + useradd -o -u "$PUID" -U -d "$NPMHOME" -s /bin/false "$NPMUSER" +fi + +log_info "Configuring $NPMGROUP group ..." +if [ "$(get_group_id "$NPMGROUP")" = '' ]; then + # Add group. This will not set the id properly if it's already taken + groupadd -f -g "$PGID" "$NPMGROUP" +else + groupmod -o -g "$PGID" "$NPMGROUP" +fi + +# Set the group ID and check it +groupmod -o -g "$PGID" "$NPMGROUP" +if [ "$(get_group_id "$NPMGROUP")" != "$PGID" ]; then + echo "ERROR: Unable to set group id properly" + exit 1 +fi + +# Set the group against the user and check it +usermod -G "$PGID" "$NPMGROUP" +if [ "$(id -g "$NPMUSER")" != "$PGID" ] ; then + echo "ERROR: Unable to set group against the user properly" + exit 1 +fi + +# Home for user +mkdir -p "$NPMHOME" +chown -R "$PUID:$PGID" "$NPMHOME" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh new file mode 100755 index 000000000..0be2af72f --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh @@ -0,0 +1,27 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Checking paths ...' + +# Ensure /data is mounted +if [ ! -d '/data' ]; then + log_fatal '/data is not mounted! Check your docker configuration.' +fi + +# Create required folders +mkdir -p \ + /data/logs \ + /data/nginx \ + /run/nginx \ + /tmp/nginx/body \ + /var/log/nginx \ + /var/lib/nginx/cache/public \ + /var/lib/nginx/cache/private \ + /var/cache/nginx/proxy_temp + +touch /var/log/nginx/error.log || true +chmod 777 /var/log/nginx/error.log || true +chmod -R 777 /var/cache/nginx || true +chmod 644 /etc/logrotate.d/nginx-proxy-manager diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh new file mode 100755 index 000000000..4a02b5ff0 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh @@ -0,0 +1,23 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Setting ownership ...' + +# root +chown root /tmp/nginx + +# npm user and group +chown -R "$PUID:$PGID" /data +chown -R "$PUID:$PGID" /run/nginx +chown -R "$PUID:$PGID" /tmp/nginx +chown -R "$PUID:$PGID" /var/cache/nginx +chown -R "$PUID:$PGID" /var/lib/logrotate +chown -R "$PUID:$PGID" /var/lib/nginx +chown -R "$PUID:$PGID" /var/log/nginx + +# Don't chown entire /etc/nginx folder as this causes crashes on some systems +chown -R "$PUID:$PGID" /etc/nginx/nginx +chown -R "$PUID:$PGID" /etc/nginx/nginx.conf +chown -R "$PUID:$PGID" /etc/nginx/conf.d diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh new file mode 100755 index 000000000..5a8741355 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh @@ -0,0 +1,34 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Dynamic resolvers ...' + +# Dynamically generate resolvers file, if resolver is IPv6, enclose in `[]` +# thanks @tfmm +if [ "$(is_true "$NPM_DISABLE_IPV6")" = '1' ]; then + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) ipv6=off valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf +else + echo resolver "$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /etc/nginx/conf.d/include/resolvers.conf +fi + +# Fire off acme.sh wrapper script to "install" itself if required +acme.sh -h > /dev/null 2>&1 + +# Generate IP Ranges from online CDN services +# continue on error, as this could be due to network errors +# and can be attempted again with a docker restart +rm -rf /etc/nginx/conf.d/include/ipranges.conf +set +e +RC=0 +if [ "$(is_true "$DEVELOPMENT")" = '1' ]; then + echo '# ignored in development mode' > /etc/nginx/conf.d/include/ipranges.conf +else + /app/bin/ipranges > /etc/nginx/conf.d/include/ipranges.conf + RC=$? +fi +if [ "$RC" != '0' ]; then + log_warn 'Generation of IP Ranges file has an error. Check output of /etc/nginx/conf.d/include/ipranges.conf for more information.' +fi +set -e diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv46.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv46.sh new file mode 100755 index 000000000..0199e4e59 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv46.sh @@ -0,0 +1,58 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +# This command reads the `NPM_DISABLE_IPV4` and `NPM_DISABLE_IPV6`` env vars and will either enable +# or disable ipv6 in all nginx configs based on this setting. + +set -e + +log_info 'IPv4/IPv6 ...' + +DIS_4=$(is_true "$NPM_DISABLE_IPV4") +DIS_6=$(is_true "$NPM_DISABLE_IPV6") + +# Ensure someone didn't misconfigure the settings +if [ "$DIS_4" = "1" ] && [ "$DIS_6" = "1" ]; then + log_fatal 'NPM_DISABLE_IPV4 and NPM_DISABLE_IPV6 cannot both be set!' +fi + +process_folder () { + FILES=$(find "$1" -type f -name "*.conf") + SED_REGEX= + + # IPV4 ... + if [ "$DIS_4" = "1" ]; then + echo "Disabling IPV4 in hosts in: $1" + SED_REGEX='s/^([^#]*)listen ([0-9]+)/\1#listen \2/g' + else + echo "Enabling IPV4 in hosts in: $1" + SED_REGEX='s/^(\s*)#listen ([0-9]+)/\1listen \2/g' + fi + + for FILE in $FILES + do + echo " - ${FILE}" + echo "$(sed -E "$SED_REGEX" "$FILE")" > $FILE + done + + # IPV6 ... + if [ "$DIS_6" = "1" ]; then + echo "Disabling IPV6 in hosts in: $1" + SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' + else + echo "Enabling IPV6 in hosts in: $1" + SED_REGEX='s/^(\s*)#listen \[::\]/\1listen [::]/g' + fi + + for FILE in $FILES + do + echo " - ${FILE}" + echo "$(sed -E "$SED_REGEX" "$FILE")" > $FILE + done + + # ensure the files are still owned by the npm user + chown -R "$PUID:$PGID" "$1" +} + +process_folder /etc/nginx/conf.d +process_folder /data/nginx diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-fail2ban.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-fail2ban.sh new file mode 100755 index 000000000..70071f11b --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-fail2ban.sh @@ -0,0 +1,13 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Fail2ban configuration ...' + +mkdir -p /fail2ban/{action.d,filter.d,jail.d,log} +chown -R "$PUID:$PGID" /fail2ban +mkdir -p /var/run/fail2ban +mkdir -p /data/logs/fail2ban +chown nobody:nogroup /data/logs/fail2ban +chmod 02755 /data/logs/fail2ban diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh new file mode 100755 index 000000000..4e69c0f83 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh @@ -0,0 +1,24 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e +set +x + +. /etc/os-release + +echo " +------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +Version: ${NPM_BUILD_VERSION:-3.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00} +User: $NPMUSER PUID:$PUID ID:$(id -u "$NPMUSER") GROUP:$(id -g "$NPMUSER") +Group: $NPMGROUP PGID:$PGID ID:$(get_group_id "$NPMGROUP") +OpenResty: ${OPENRESTY_VERSION:-unknown} +Debian: ${VERSION_ID:-unknown} +Kernel: $(uname -r) +------------------------------------- +" diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up new file mode 100644 index 000000000..896a01b60 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/up @@ -0,0 +1,2 @@ +# shellcheck shell=bash +/etc/s6-overlay/s6-rc.d/prepare/00-all.sh diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/backend new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/fail2ban b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/fail2ban new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/frontend new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/nginx new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare b/docker/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/prepare new file mode 100644 index 000000000..e69de29bb diff --git a/docker/rootfs/etc/services.d/frontend/finish b/docker/rootfs/etc/services.d/frontend/finish deleted file mode 100755 index bca9a35db..000000000 --- a/docker/rootfs/etc/services.d/frontend/finish +++ /dev/null @@ -1,6 +0,0 @@ -#!/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/frontend/run b/docker/rootfs/etc/services.d/frontend/run deleted file mode 100755 index a666d53ef..000000000 --- a/docker/rootfs/etc/services.d/frontend/run +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/with-contenv bash - -# This service is DEVELOPMENT only. - -if [ "$DEVELOPMENT" == "true" ]; then - cd /app/frontend || exit 1 - # If yarn install fails: add --verbose --network-concurrency 1 - yarn install - yarn watch -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 7d442d6af..000000000 --- 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 e365f4fbb..000000000 --- a/docker/rootfs/etc/services.d/manager/run +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/with-contenv bash - -mkdir -p /data/letsencrypt-acme-challenge - -cd /app || echo - -if [ "$DEVELOPMENT" == "true" ]; then - cd /app || exit 1 - # If yarn install fails: add --verbose --network-concurrency 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 63b10de42..000000000 --- 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/run b/docker/rootfs/etc/services.d/nginx/run deleted file mode 100755 index 51ca5ea18..000000000 --- a/docker/rootfs/etc/services.d/nginx/run +++ /dev/null @@ -1,49 +0,0 @@ -#!/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" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf) valid=10s;" > /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=localhost/OU=localhost/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 diff --git a/docker/rootfs/fail2ban/action.d/abuseipdb.conf b/docker/rootfs/fail2ban/action.d/abuseipdb.conf new file mode 100644 index 000000000..1702884fa --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/abuseipdb.conf @@ -0,0 +1,105 @@ +## Version 2022/08/06 +# Fail2ban configuration file +# +# Action to report IP address to abuseipdb.com +# You must sign up to obtain an API key from abuseipdb.com. +# +# NOTE: These reports may include sensitive Info. +# If you want cleaner reports that ensure no user data see the helper script at the below website. +# +# IMPORTANT: +# +# Reporting an IP of abuse is a serious complaint. Make sure that it is +# serious. Fail2ban developers and network owners recommend you only use this +# action for: +# * The recidive where the IP has been banned multiple times +# * Where maxretry has been set quite high, beyond the normal user typing +# password incorrectly. +# * For filters that have a low likelihood of receiving human errors +# +# This action relies on a api_key being added to the above action conf, +# and the appropriate categories set. +# +# Example, for ssh bruteforce (in section [sshd] of `jail.local`): +# action = %(known/action)s +# abuseipdb[abuseipdb_apikey="my-api-key", abuseipdb_category="18,22"] +# +# See below for categories. +# +# Added to fail2ban by Andrew James Collett (ajcollett) + +## abuseIPDB Categories, `the abuseipdb_category` MUST be set in the jail.conf action call. +# Example, for ssh bruteforce: action = %(action_abuseipdb)s[abuseipdb_category="18,22"] +# ID Title Description +# 3 Fraud Orders +# 4 DDoS Attack +# 9 Open Proxy +# 10 Web Spam +# 11 Email Spam +# 14 Port Scan +# 18 Brute-Force +# 19 Bad Web Bot +# 20 Exploited Host +# 21 Web App Attack +# 22 SSH Secure Shell (SSH) abuse. Use this category in combination with more specific categories. +# 23 IoT Targeted +# See https://abuseipdb.com/categories for more descriptions + +[Definition] + +# bypass action for restored tickets +norestored = 1 + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +actionstart = + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +actionstop = + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# +# ** IMPORTANT! ** +# +# By default, this posts directly to AbuseIPDB's API, unfortunately +# this results in a lot of backslashes/escapes appearing in the +# reports. This also may include info like your hostname. +# If you have your own web server with PHP available, you can +# use my (Shaun's) helper PHP script by commenting out the first #actionban +# line below, uncommenting the second one, and pointing the URL at +# wherever you install the helper script. For the PHP helper script, see +# +# +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = lgm=$(printf '%%.1000s\n...' ""); curl -sSf "https://api.abuseipdb.com/api/v2/report" -H "Accept: application/json" -H "Key: " --data-urlencode "comment=$lgm" --data-urlencode "ip=" --data "categories=" + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = + +[Init] +# Option: abuseipdb_apikey +# Notes Your API key from abuseipdb.com +# Values: STRING Default: None +# Register for abuseipdb [https://www.abuseipdb.com], get api key and set below. +# You will need to set the category in the action call. +abuseipdb_apikey = diff --git a/docker/rootfs/fail2ban/action.d/apf.conf b/docker/rootfs/fail2ban/action.d/apf.conf new file mode 100644 index 000000000..5ce02626e --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/apf.conf @@ -0,0 +1,26 @@ +## Version 2022/08/06 +# Fail2Ban configuration file +# https://www.rfxn.com/projects/advanced-policy-firewall/ +# +# Note: APF doesn't play nicely with other actions. It has been observed to +# remove bans created by other iptables based actions. If you are going to use +# this action, use it for all of your jails. +# +# DON'T MIX APF and other IPTABLES based actions +[Definition] + +actionstart = +actionstop = +actioncheck = +actionban = apf --deny "banned by Fail2Ban " +actionunban = apf --remove + +[Init] + +# Name used in APF configuration +# +name = default + +# DEV NOTES: +# +# Author: Mark McKinstry diff --git a/docker/rootfs/fail2ban/action.d/apprise-api.conf b/docker/rootfs/fail2ban/action.d/apprise-api.conf new file mode 100644 index 000000000..767aafc36 --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/apprise-api.conf @@ -0,0 +1,60 @@ +## Version 2022/08/06 +# Fail2Ban action configuration for apprise-api +# Author: Roxedus https://github.com/Roxedus +# Modified by: nemchik https://github.com/nemchik + +[Definition] + +# Option: actionstart +# Notes.: command executed once at the start of Fail2Ban. +# Values: CMD +# +actionstart = curl -X POST -d '{"tag": "", "type": "info", "body": "The jail as been started successfully."}' \ + -H "Content-Type: application/json" \ + + +# Option: actionstop +# Notes.: command executed once at the end of Fail2Ban +# Values: CMD +# +actionstop = curl -X POST -d '{"tag": "", "type": "info", "body": "The jail has been stopped."}' \ + -H "Content-Type: application/json" \ + + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# + +actionban = curl -X POST -d '{"tag": "", "type": "warning", "body": "The IP has just been banned from after attempts."}' \ + -H "Content-Type: application/json" \ + + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# + +actionunban = curl -X POST -d '{"tag": "", "type": "success", "body": "The IP has just been unbanned from ."}' \ + -H "Content-Type: application/json" \ + + +[Init] + +proto = http +host = apprise +port = 8000 +key = apprise +url = ://:/notify/ +#tag = fail2ban +tag = all diff --git a/docker/rootfs/fail2ban/action.d/apprise.conf b/docker/rootfs/fail2ban/action.d/apprise.conf new file mode 100644 index 000000000..3a6bdebe4 --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/apprise.conf @@ -0,0 +1,50 @@ +## Version 2022/08/06 +# Fail2Ban configuration file +# +# Author: Chris Caron +# +# + +[Definition] + +# Option: actionstart +# Notes.: command executed once at the start of Fail2Ban. +# Values: CMD +# +actionstart = printf %%b "The jail as been started successfully." | -t "[Fail2Ban] : started on `uname -n`" + +# Option: actionstop +# Notes.: command executed once at the end of Fail2Ban +# Values: CMD +# +actionstop = printf %%b "The jail has been stopped." | -t "[Fail2Ban] : stopped on `uname -n`" + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = printf %%b "The IP has just been banned by Fail2Ban after attempts against " | -n "warning" -t "[Fail2Ban] : banned from `uname -n`" + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = + +[Init] + +# Define location of the default apprise configuration file to use +# +config = /etc/fail2ban/apprise.conf +# +apprise = apprise -c "" diff --git a/docker/rootfs/fail2ban/action.d/blocklist_de.conf b/docker/rootfs/fail2ban/action.d/blocklist_de.conf new file mode 100644 index 000000000..d2b0b6805 --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/blocklist_de.conf @@ -0,0 +1,85 @@ +## Version 2022/08/06 +# Fail2Ban configuration file +# +# Author: Steven Hiscocks +# +# + +# Action to report IP address to blocklist.de +# Blocklist.de must be signed up to at www.blocklist.de +# Once registered, one or more servers can be added. +# This action requires the server 'email address' and the associated apikey. +# +# From blocklist.de: +# www.blocklist.de is a free and voluntary service provided by a +# Fraud/Abuse-specialist, whose servers are often attacked on SSH-, +# Mail-Login-, FTP-, Webserver- and other services. +# The mission is to report all attacks to the abuse departments of the +# infected PCs/servers to ensure that the responsible provider can inform +# the customer about the infection and disable them +# +# IMPORTANT: +# +# Reporting an IP of abuse is a serious complaint. Make sure that it is +# serious. Fail2ban developers and network owners recommend you only use this +# action for: +# * The recidive where the IP has been banned multiple times +# * Where maxretry has been set quite high, beyond the normal user typing +# password incorrectly. +# * For filters that have a low likelihood of receiving human errors +# + +[Definition] + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +actionstart = + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +actionstop = + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = curl --fail --data-urlencode "server=" --data "apikey=" --data "service=" --data "ip=" --data-urlencode "logs=
" --data 'format=text' --user-agent "" "https://www.blocklist.de/en/httpreports.html" + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = + +# Option: email +# Notes server email address, as per blocklist.de account +# Values: STRING Default: None +# +#email = + +# Option: apikey +# Notes your user blocklist.de user account apikey +# Values: STRING Default: None +# +#apikey = + +# Option: service +# Notes service name you are reporting on, typically aligns with filter name +# see http://www.blocklist.de/en/httpreports.html for full list +# Values: STRING Default: None +# +#service = diff --git a/docker/rootfs/fail2ban/action.d/bsd-ipfw.conf b/docker/rootfs/fail2ban/action.d/bsd-ipfw.conf new file mode 100644 index 000000000..9097ed437 --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/bsd-ipfw.conf @@ -0,0 +1,95 @@ +## Version 2022/08/06 +# Fail2Ban configuration file +# +# Author: Nick Munger +# Modified by: Ken Menzel +# Daniel Black (start/stop) +# Fabian Wenk (many ideas as per fail2ban users list) +# +# Ensure firewall_enable="YES" in the top of /etc/rc.conf +# + +[Definition] + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +actionstart = ipfw show | fgrep -c -m 1 -s 'table()' > /dev/null 2>&1 || ( + num=$(ipfw show | awk 'BEGIN { b = } { if ($1 == b) { b = $1 + 1 } } END { print b }'); + ipfw -q add "$num" from table\(
\) to me ; echo "$num" > "" + ) + + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +actionstop = [ ! -f ] || ( read num < ""
ipfw -q delete $num
rm "" ) + + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +# requires an ipfw rule like "deny ip from table(1) to me" +actionban = e=`ipfw table
add 2>&1`; x=$?; [ $x -eq 0 -o "$e" = 'ipfw: setsockopt(IP_FW_TABLE_XADD): File exists' ] || echo "$e" | grep -q "record already exists" || { echo "$e" 1>&2; exit $x; } + + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = e=`ipfw table
delete 2>&1`; x=$?; [ $x -eq 0 -o "$e" = 'ipfw: setsockopt(IP_FW_TABLE_XDEL): No such process' ] || echo "$e" | grep -q "record not found" || { echo "$e" 1>&2; exit $x; } + +[Init] +# Option: table +# Notes: The ipfw table to use. If a ipfw rule using this table already exists, +# this action will not create a ipfw rule to block it and the following +# options will have no effect. +# Values: NUM +table = 1 + +# Option: port +# Notes.: Specifies port to monitor. Blank indicate block all ports. +# Values: [ NUM | STRING ] +# +port = + +# Option: startstatefile +# Notes: A file to indicate that the table rule that was added. Ensure it is unique per table. +# Values: STRING +startstatefile = /var/run/fail2ban/ipfw-started-table_
+ +# Option: block +# Notes: This is how much to block. +# Can be "ip", "tcp", "udp" or various other options. +# Values: STRING +block = ip + +# Option: blocktype +# Notes.: How to block the traffic. Use a action from man 5 ipfw +# Common values: deny, unreach port, reset +# ACTION defination at the top of man ipfw for allowed values. +# Values: STRING +# +blocktype = unreach port + +# Option: lowest_rule_num +# Notes: When fail2ban starts with action and there is no rule for the given table yet +# then fail2ban will start looking for an empty slot starting with this rule number. +# Values: NUM +lowest_rule_num = 111 + + diff --git a/docker/rootfs/fail2ban/action.d/cloudflare-token.conf b/docker/rootfs/fail2ban/action.d/cloudflare-token.conf new file mode 100644 index 000000000..8b83abf9a --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/cloudflare-token.conf @@ -0,0 +1,93 @@ +## Version 2022/12/15 +# +# Author: Logic-32 +# +# IMPORTANT +# +# Please set jail.local's permission to 640 because it contains your CF API token. +# +# This action depends on curl. +# +# To get your Cloudflare API token: https://developers.cloudflare.com/api/tokens/create/ +# +# Cloudflare Firewall API: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/endpoints/ + +[Definition] + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +actionstart = + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +actionstop = + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: IP address +# number of failures +#
+_nft_get_handle_id = grep -oP '@\s+.*\s+\Khandle\s+(\d+)$' + +_nft_add_set = add set
\{ type \; \} + <_nft_for_proto--iter> + add rule
%(rule_stat)s + <_nft_for_proto--done> +_nft_del_set = { %(_nft_list)s | %(_nft_get_handle_id)s; } | while read -r hdl; do + delete rule
$hdl; done + delete set
+ +# Option: _nft_shutdown_table +# Notes.: command executed after the stop in order to delete table (it checks that no sets are available): +# Values: CMD +# +_nft_shutdown_table = { list table
| grep -qP '^\s+set\s+'; } || { + delete table
+ } + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +actionstart = add table
+ -- add chain
\{ type hook priority \; \} + %(_nft_add_set)s + +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action); +# uses `nft flush set ...` and as fallback (e. g. unsupported) recreates the set (with references) +# Values: CMD +# +actionflush = { flush set
2> /dev/null; } || { + %(_nft_del_set)s + %(_nft_add_set)s + } + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +actionstop = %(_nft_del_set)s + <_nft_shutdown_table> + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = list chain
| grep -q '@[ \t]' + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionban = add element
\{ \} + +# Option: actionunban +# Notes.: command executed when unbanning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: See jail.conf(5) man page +# Values: CMD +# +actionunban = delete element
\{ \} + +[Init] + +# Option: table +# Notes.: main table to store chain and sets (automatically created on demand) +# Values: STRING Default: f2b-table +table = f2b-table + +# Option: table_family +# Notes.: address family to work in +# Values: [ip | ip6 | inet] Default: inet +table_family = inet + +# Option: chain +# Notes.: main chain to store rules +# Values: STRING Default: f2b-chain +chain = f2b-chain + +# Option: chain_type +# Notes.: refers to the kind of chain to be created +# Values: [filter | route | nat] Default: filter +# +chain_type = filter + +# Option: chain_hook +# Notes.: refers to the kind of chain to be created +# Values: [ prerouting | input | forward | output | postrouting ] Default: input +# +chain_hook = input + +# Option: chain_priority +# Notes.: priority in the chain. +# Values: NUMBER Default: -1 +# +chain_priority = -1 + +# Option: addr_type +# Notes.: address type to work with +# Values: [ipv4_addr | ipv6_addr] Default: ipv4_addr +# +addr_type = ipv4_addr + +# Default name of the filtering set +# +name = default + +# Option: port +# Notes.: specifies port to monitor +# Values: [ NUM | STRING ] Default: +# +port = ssh + +# Option: protocol +# Notes.: internally used by config reader for interpolations. +# Values: [ tcp | udp ] Default: tcp +# +protocol = tcp + +# Option: blocktype +# Note: This is what the action does with rules. This can be any jump target +# as per the nftables man page (section 8). Common values are drop, +# reject, reject with icmpx type host-unreachable, redirect to 2222 +# Values: STRING +blocktype = reject + +# Option: nftables +# Notes.: Actual command to be executed, including common to all calls options +# Values: STRING +nftables = nft + +# Option: addr_set +# Notes.: The name of the nft set used to store banned addresses +# Values: STRING +addr_set = addr-set- + +# Option: addr_family +# Notes.: The family of the banned addresses +# Values: [ ip | ip6 ] +addr_family = ip + +[Init?family=inet6] +addr_family = ip6 +addr_type = ipv6_addr +addr_set = addr6-set- diff --git a/docker/rootfs/fail2ban/action.d/nginx-block-map.conf b/docker/rootfs/fail2ban/action.d/nginx-block-map.conf new file mode 100644 index 000000000..05e9b03d5 --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/nginx-block-map.conf @@ -0,0 +1,118 @@ +## Version 2022/08/06 +# Fail2Ban configuration file for black-listing via nginx +# +# Author: Serg G. Brester (aka sebres) +# +# To use 'nginx-block-map' action you should define some special blocks in your nginx configuration, +# and use it hereafter in your locations (to notify fail2ban by failure, resp. nginx by ban). +# +# Example (argument "token_id" resp. cookie "session_id" used here as unique identifier for user): +# +# http { +# ... +# # maps to check user is blacklisted (banned in f2b): +# #map $arg_token_id $blck_lst_tok { include blacklisted-tokens.map; } +# map $cookie_session_id $blck_lst_ses { include blacklisted-sessions.map; } +# ... +# # special log-format to notify fail2ban about failures: +# log_format f2b_session_errors '$msec failure "$cookie_session_id" - $remote_addr - $remote_user ' +# ;# '"$request" $status $bytes_sent ' +# # '"$http_referer" "$http_user_agent"'; +# +# # location checking blacklisted values: +# location ... { +# # check banned sessionid: +# if ($blck_lst_ses != "") { +# try_files "" @f2b-banned; +# } +# ... +# # notify fail2ban about a failure inside nginx: +# error_page 401 = @notify-f2b; +# ... +# } +# ... +# # location for return with "403 Forbidden" if banned: +# location @f2b-banned { +# default_type text/html; +# return 403 "
+# +# You are banned!
"; +# } +# ... +# # location to notify fail2ban about a failure inside nginx: +# location @notify-f2b { +# access_log /var/log/nginx/f2b-auth-errors.log f2b_session_errors; +# } +# } +# ... +# +# Note that quote-character (and possibly other special characters) are not allowed currently as session-id. +# Thus please add any session-id validation rule in your locations (or in the corresponding backend-service), +# like in example below: +# +# location ... { +# if ($cookie_session_id !~ "^[\w\-]+$") { +# return 403 "Wrong session-id" +# } +# ... +# } +# +# The parameters for jail corresponding log-format (f2b_session_errors): +# +# [nginx-blck-lst] +# filter = +# datepattern = ^Epoch +# failregex = ^ failure "[^"]+" - +# usedns = no +# +# The same log-file can be used for IP-related jail (additionally to session-related, to ban very bad IPs): +# +# [nginx-blck-ip] +# maxretry = 100 +# filter = +# datepattern = ^Epoch +# failregex = ^ failure "[^"]+" - +# usedns = no +# + +[Definition] + +# path to configuration of nginx (used to target nginx-instance in multi-instance system, +# and as path for the blacklisted map): +srv_cfg_path = /etc/nginx/ + +# cmd-line arguments to supply to test/reload nginx: +#srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf +srv_cmd = nginx + +# pid file (used to check nginx is running): +srv_pid = /run/nginx.pid + +# command used to check whether nginx is running and configuration is valid: +srv_is_running = [ -f "%(srv_pid)s" ] +srv_check_cmd = %(srv_is_running)s && %(srv_cmd)s -qt + +# first test nginx is running and configuration is correct, hereafter send reload signal: +blck_lst_reload = %(srv_check_cmd)s; if [ $? -eq 0 ]; then + %(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi; + fi; + +# map-file for nginx, can be redefined using `action = nginx-block-map[blck_lst_file="/path/file.map"]`: +blck_lst_file = %(srv_cfg_path)s/blacklisted-sessions.map + +# Action definition: + +actionstart_on_demand = false +actionstart = touch '%(blck_lst_file)s' + +actionflush = truncate -s 0 '%(blck_lst_file)s'; %(blck_lst_reload)s + +actionstop = %(actionflush)s + +actioncheck = + +_echo_blck_row = printf '\%%s 1;\n' "" + +actionban = %(_echo_blck_row)s >> '%(blck_lst_file)s'; %(blck_lst_reload)s + +actionunban = id=$(%(_echo_blck_row)s | sed -e 's/[]\/$*.^|[]/\\&/g'); sed -i "/^$id$/d" %(blck_lst_file)s; %(blck_lst_reload)s diff --git a/docker/rootfs/fail2ban/action.d/npf.conf b/docker/rootfs/fail2ban/action.d/npf.conf new file mode 100644 index 000000000..fcedc12dc --- /dev/null +++ b/docker/rootfs/fail2ban/action.d/npf.conf @@ -0,0 +1,62 @@ +## Version 2022/08/06 +# Fail2Ban configuration file +# +# NetBSD npf ban/unban +# +# Author: Nils Ratusznik +# Based on pf.conf action file +# + +[Definition] + +# Option: actionstart +# Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). +# Values: CMD +# +# we don't enable NPF automatically, as it will be enabled elsewhere +actionstart = + + +# Option: actionstop +# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) +# Values: CMD +# +# we don't disable NPF automatically either +actionstop = + + +# Option: actioncheck +# Notes.: command executed once before each actionban command +# Values: CMD +# +actioncheck = + + +# Option: actionban +# Notes.: command executed when banning an IP. Take care that the +# command is executed with Fail2Ban user rights. +# Tags: IP address +# number of failures +#
- - - 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 862ffc221..000000000 --- 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 ec3cf2a2b..000000000 --- a/frontend/js/app/audit-log/list/main.ejs +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - 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 9d3e26fb1..000000000 --- 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 8d182b595..000000000 --- a/frontend/js/app/audit-log/main.ejs +++ /dev/null @@ -1,25 +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 0d03c5ca8..000000000 --- a/frontend/js/app/audit-log/main.js +++ /dev/null @@ -1,82 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.AuditLog.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new AuditLogModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showAuditLog(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - this.showChildView('list_region', new EmptyView({ - title: App.i18n('audit-log', 'empty'), - subtitle: App.i18n('audit-log', 'empty-subtitle') - })); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['user'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - onRender: function () { - let view = this; - - view.fetch(['user']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - }) - .catch(err => { - view.showError(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 98a2d9734..000000000 --- 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 815cdfac9..000000000 --- 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 6d1fbc4f9..000000000 --- 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 ccb2978a8..000000000 --- a/frontend/js/app/controller.js +++ /dev/null @@ -1,447 +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})); - }); - } - }, - - /** - * Certificate Test Reachability - * - * @param model - */ - showNginxCertificateTestReachability: function (model) { - if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) { - require(['./main', './nginx/certificates/test'], 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 c00aa6d0f..000000000 --- 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 c2e82f855..000000000 --- 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 11633dfcc..000000000 --- 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 74998d65b..000000000 --- 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 f7fd709bd..000000000 --- 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 6fa85fc8c..000000000 --- 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 6fb79e667..000000000 --- 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 b0f54374c..000000000 --- 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 c63cdc079..000000000 --- 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 e85b4f620..000000000 --- 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 3833549a6..000000000 --- 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 4af91ab17..000000000 --- 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 79220b14b..000000000 --- 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 bb0755481..000000000 --- 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 6b767b83f..000000000 --- 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 b4c00e2e3..000000000 --- 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 c2435ecb3..000000000 --- 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 f15238dcc..000000000 --- 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 2ee37a50a..000000000 --- a/frontend/js/app/nginx/access/list/item.ejs +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - -<% 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 4f68aead7..000000000 --- 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 7988e0c28..000000000 --- a/frontend/js/app/nginx/access/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - <% 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 577a77ef2..000000000 --- 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 975859364..000000000 --- a/frontend/js/app/nginx/access/main.ejs +++ /dev/null @@ -1,28 +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 513f58659..000000000 --- a/frontend/js/app/nginx/access/main.js +++ /dev/null @@ -1,108 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.AccessLists.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new AccessListModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxAccess(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('access_lists'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'items', 'clients'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('access_lists') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'items', 'clients']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 aa4b53ad2..000000000 --- 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 b4e068660..000000000 --- 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 89a2e5e83..000000000 --- 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 7fc12785b..000000000 --- a/frontend/js/app/nginx/certificates/form.ejs +++ /dev/null @@ -1,185 +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 a56c3f8e0..000000000 --- a/frontend/js/app/nginx/certificates/form.js +++ /dev/null @@ -1,294 +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"]', - test_domains_container: '.test-domains-container', - test_domains_button: '.test-domains', - 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(); - this.ui.test_domains_container.hide(); - } else { - this.ui.dns_provider.prop('required', false); - this.ui.dns_provider_credentials.prop('required', false); - this.ui.dns_challenge_content.hide(); - this.ui.test_domains_container.show(); - } - }, - - '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(); - }); - }, - 'click @ui.test_domains_button': function (e) { - e.preventDefault(); - const domainNames = this.ui.domain_names[0].value.split(','); - if (domainNames && domainNames.length > 0) { - this.model.set('domain_names', domainNames); - this.model.set('back_to_add', true); - App.Controller.showNginxCertificateTestReachability(this.model); - } - }, - 'change @ui.domain_names': function(e){ - const domainNames = e.target.value.split(','); - if (domainNames && domainNames.length > 0) { - this.ui.test_domains_button.prop('disabled', false); - } else { - this.ui.test_domains_button.prop('disabled', true); - } - }, - '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(); - if (this.ui.domain_names[0]) { - const domainNames = this.ui.domain_names[0].value.split(','); - if (!domainNames || domainNames.length === 0 || (domainNames.length === 1 && domainNames[0] === "")) { - this.ui.test_domains_button.prop('disabled', true); - } - } - }, - - 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 bdeeecad0..000000000 --- a/frontend/js/app/nginx/certificates/list/item.ejs +++ /dev/null @@ -1,54 +0,0 @@ - - - - -<% 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 7fa1c6814..000000000 --- a/frontend/js/app/nginx/certificates/list/item.js +++ /dev/null @@ -1,58 +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', - download: 'a.download', - test: 'a.test' - }, - - 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(); - }, - - 'click @ui.download': function (e) { - e.preventDefault(); - App.Api.Nginx.Certificates.download(this.model.get('id')); - }, - - 'click @ui.test': function (e) { - e.preventDefault(); - App.Controller.showNginxCertificateTestReachability(this.model); - }, - }, - - 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 aa49a27fb..000000000 --- a/frontend/js/app/nginx/certificates/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - <% 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 d96b43e8f..000000000 --- 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 dbd6fa85d..000000000 --- a/frontend/js/app/nginx/certificates/main.ejs +++ /dev/null @@ -1,36 +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 89562768b..000000000 --- a/frontend/js/app/nginx/certificates/main.js +++ /dev/null @@ -1,109 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.Certificates.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new CertificateModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxCertificates(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('certificates'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('certificates') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 4af186d09..000000000 --- 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 736328817..000000000 --- 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/certificates/test.ejs b/frontend/js/app/nginx/certificates/test.ejs deleted file mode 100644 index 6661f625f..000000000 --- a/frontend/js/app/nginx/certificates/test.ejs +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/frontend/js/app/nginx/certificates/test.js b/frontend/js/app/nginx/certificates/test.js deleted file mode 100644 index 0886d26f5..000000000 --- a/frontend/js/app/nginx/certificates/test.js +++ /dev/null @@ -1,75 +0,0 @@ -const Mn = require('backbone.marionette'); -const App = require('../../main'); -const template = require('./test.ejs'); - -module.exports = Mn.View.extend({ - template: template, - className: 'modal-dialog', - - ui: { - waiting: '.waiting', - error: '.error', - success: '.success', - close: 'button.cancel' - }, - - events: { - 'click @ui.close': function (e) { - e.preventDefault(); - if (this.model.get('back_to_add')) { - App.Controller.showNginxCertificateForm(this.model); - } else { - App.UI.closeModal(); - } - }, - }, - - onRender: function () { - this.ui.error.hide(); - this.ui.success.hide(); - - App.Api.Nginx.Certificates.testHttpChallenge(this.model.get('domain_names')) - .then((result) => { - let allOk = true; - let text = ''; - - for (const domain in result) { - const status = result[domain]; - if (status === 'ok') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-ok')}

`; - } else { - allOk = false; - if (status === 'no-host') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-not-resolved')}

`; - } else if (status === 'failed') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-failed-to-check')}

`; - } else if (status === '404') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-404')}

`; - } else if (status === 'wrong-data') { - text += `

${domain}: ${App.i18n('certificates', 'reachability-wrong-data')}

`; - } else if (status.startsWith('other:')) { - const code = status.substring(6); - text += `

${domain}: ${App.i18n('certificates', 'reachability-other', {code})}

`; - } else { - // This should never happen - text += `

${domain}: ?

`; - } - } - } - - this.ui.waiting.hide(); - if (allOk) { - this.ui.success.html(text).show(); - } else { - this.ui.error.html(text).show(); - } - this.ui.close.prop('disabled', false); - }) - .catch((e) => { - console.error(e); - this.ui.waiting.hide(); - this.ui.error.text(App.i18n('certificates', 'reachability-failed-to-reach-api')).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 4bebb4360..000000000 --- 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 d497d0685..000000000 --- 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 253c4b6fb..000000000 --- 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 8f6774f68..000000000 --- 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 d447bd1e3..000000000 --- a/frontend/js/app/nginx/dead/list/item.ejs +++ /dev/null @@ -1,54 +0,0 @@ - - - - -<% 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 a477dbfa3..000000000 --- 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 e018a74b0..000000000 --- a/frontend/js/app/nginx/dead/list/main.ejs +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - <% 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 579314199..000000000 --- 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 4c5d1ad1f..000000000 --- a/frontend/js/app/nginx/dead/main.ejs +++ /dev/null @@ -1,28 +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 e4d0c010e..000000000 --- a/frontend/js/app/nginx/dead/main.js +++ /dev/null @@ -1,108 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.DeadHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new DeadHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxDead(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('dead_hosts'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('dead_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 e5a7e1163..000000000 --- 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 74da297c2..000000000 --- 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 63a8e020a..000000000 --- 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 56868f552..000000000 --- a/frontend/js/app/nginx/proxy/form.ejs +++ /dev/null @@ -1,281 +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 1dfb5c189..000000000 --- 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 a59368048..000000000 --- a/frontend/js/app/nginx/proxy/list/item.ejs +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - -<% 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 37d199b4a..000000000 --- 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 6de5b9c68..000000000 --- a/frontend/js/app/nginx/proxy/list/main.ejs +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - <% 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 09e984e62..000000000 --- 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 39445f7be..000000000 --- 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 e9513a480..000000000 --- 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 4ecb9036a..000000000 --- a/frontend/js/app/nginx/proxy/main.ejs +++ /dev/null @@ -1,28 +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 baf671013..000000000 --- a/frontend/js/app/nginx/proxy/main.js +++ /dev/null @@ -1,108 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.ProxyHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new ProxyHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxProxy(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('proxy_hosts'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'access_list', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'access_list', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 782d8435d..000000000 --- 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 6d2862f68..000000000 --- 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 7e1907193..000000000 --- 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 1f81feebc..000000000 --- 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 4f25d973e..000000000 --- a/frontend/js/app/nginx/redirection/list/item.ejs +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - -<% 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 05adc2511..000000000 --- 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 8b6930d60..000000000 --- a/frontend/js/app/nginx/redirection/list/main.ejs +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - <% 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 d368cf6aa..000000000 --- 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 87e28229a..000000000 --- a/frontend/js/app/nginx/redirection/main.ejs +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-

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

-
- -
- - - - -
- - - <% if (showAddButton) { %> - <%- i18n('redirection-hosts', 'add') %> - <% } %> -
-
-
-
-
-
- -
-
-
-
diff --git a/frontend/js/app/nginx/redirection/main.js b/frontend/js/app/nginx/redirection/main.js deleted file mode 100644 index 1f5351a73..000000000 --- a/frontend/js/app/nginx/redirection/main.js +++ /dev/null @@ -1,107 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.RedirectionHosts.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new RedirectionHostModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxRedirection(); - } - })); - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('redirection_hosts'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner', 'certificate'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('proxy_hosts') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner', 'certificate']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 d7ba3a217..000000000 --- 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 71eff18cc..000000000 --- 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 eb80c3737..000000000 --- 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 be8fc8bc2..000000000 --- a/frontend/js/app/nginx/stream/form.js +++ /dev/null @@ -1,84 +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', - forwarding_host: 'input[name="forwarding_host"]', - 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'); - }); - } - }, - - 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 a8ff83d4c..000000000 --- a/frontend/js/app/nginx/stream/list/item.ejs +++ /dev/null @@ -1,53 +0,0 @@ - - - - - -<% 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 a6892ee2a..000000000 --- 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 5304f6145..000000000 --- a/frontend/js/app/nginx/stream/list/main.ejs +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - <% 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 36be621d9..000000000 --- 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 7dc0dbe86..000000000 --- a/frontend/js/app/nginx/stream/main.ejs +++ /dev/null @@ -1,28 +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 8a86e5836..000000000 --- a/frontend/js/app/nginx/stream/main.js +++ /dev/null @@ -1,108 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Nginx.Streams.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new StreamModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showNginxStream(); - } - })); - - console.error(err); - }, - - showEmpty: function() { - let manage = App.Cache.User.canManage('streams'); - - this.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(); - } - })); - }, - - 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')); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['owner'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - templateContext: { - showAddButton: App.Cache.User.canManage('streams') - }, - - onRender: function () { - let view = this; - - view.fetch(['owner']) - .then(response => { - if (!view.isDestroyed()) { - if (response && response.length) { - view.showData(response); - } else { - view.showEmpty(); - } - } - }) - .catch(err => { - view.showError(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 a036bfc57..000000000 --- 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 126c9d0ac..000000000 --- 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 06a45b8bc..000000000 --- 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 4f81b4509..000000000 --- a/frontend/js/app/settings/list/item.ejs +++ /dev/null @@ -1,21 +0,0 @@ - - - \ 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 03f9ac05b..000000000 --- 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 c96e923a5..000000000 --- a/frontend/js/app/settings/list/main.ejs +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/js/app/settings/list/main.js b/frontend/js/app/settings/list/main.js deleted file mode 100644 index 9d3e26fb1..000000000 --- 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 2b02769f2..000000000 --- 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 96b2941ff..000000000 --- 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 4a56bcab6..000000000 --- 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 562e71c22..000000000 --- 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 73f515e68..000000000 --- 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 18ed2b6a6..000000000 --- a/frontend/js/app/ui/header/main.ejs +++ /dev/null @@ -1,34 +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 9779b45c3..000000000 --- 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 b62c3acda..000000000 --- a/frontend/js/app/ui/main.ejs +++ /dev/null @@ -1,21 +0,0 @@ -
- -
-
- -
-
-
- -
- -
- - diff --git a/frontend/js/app/ui/main.js b/frontend/js/app/ui/main.js deleted file mode 100644 index c90c61d5d..000000000 --- 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 671b4e3be..000000000 --- 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 dabe26d36..000000000 --- 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 c10532ef8..000000000 --- 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 e8ed5c32e..000000000 --- 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 aeb268f7c..000000000 --- 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 ef92ec3e9..000000000 --- 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 a45cc7ed7..000000000 --- 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 840307502..000000000 --- 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 b61617960..000000000 --- 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 af8049ce8..000000000 --- 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 fab5585bc..000000000 --- a/frontend/js/app/users/list/item.ejs +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/frontend/js/app/users/list/item.js b/frontend/js/app/users/list/item.js deleted file mode 100644 index 4645a5c46..000000000 --- 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 c85c9cb1c..000000000 --- a/frontend/js/app/users/list/main.ejs +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontend/js/app/users/list/main.js b/frontend/js/app/users/list/main.js deleted file mode 100644 index 9d3e26fb1..000000000 --- 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 892cb83f6..000000000 --- a/frontend/js/app/users/main.ejs +++ /dev/null @@ -1,26 +0,0 @@ -
-
-
-

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

-
- -
- - - - -
- - <%- i18n('users', 'add') %> -
-
-
-
-
-
- -
-
- -
-
diff --git a/frontend/js/app/users/main.js b/frontend/js/app/users/main.js deleted file mode 100644 index 42cb41ef1..000000000 --- a/frontend/js/app/users/main.js +++ /dev/null @@ -1,78 +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', - search: '.search-form', - query: 'input[name="source-query"]' - }, - - fetch: App.Api.Users.getAll, - - showData: function(response) { - this.showChildView('list_region', new ListView({ - collection: new UserModel.Collection(response) - })); - }, - - showError: function(err) { - this.showChildView('list_region', new ErrorView({ - code: err.code, - message: err.message, - retry: function () { - App.Controller.showUsers(); - } - })); - - console.error(err); - }, - - regions: { - list_region: '@ui.list_region' - }, - - events: { - 'click @ui.add': function (e) { - e.preventDefault(); - App.Controller.showUserForm(new UserModel.Model()); - }, - - 'submit @ui.search': function (e) { - e.preventDefault(); - let query = this.ui.query.val(); - - this.fetch(['permissions'], query) - .then(response => this.showData(response)) - .catch(err => { - this.showError(err); - }); - } - }, - - onRender: function () { - let view = this; - - view.fetch(['permissions']) - .then(response => { - if (!view.isDestroyed() && response && response.length) { - view.showData(response); - } - }) - .catch(err => { - view.showError(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 896a9633d..000000000 --- a/frontend/js/i18n/messages.json +++ /dev/null @@ -1,294 +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": "© 2022 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", - "advanced-config-var-headline": "These proxy details are available as nginx variables:", - "advanced-config-header-info": "Please note, that any add_header or set_header directives added here will not be used by nginx. You will have to add a custom location '/' and add the header in the custom config there.", - "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": "Add a path for sub-folder forwarding.\nExample: 203.0.113.25/path", - "search": "Search Host…" - }, - "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 Redirection 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.", - "search": "Search Host…" - }, - "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.", - "search": "Search Host…" - }, - "streams": { - "title": "Streams", - "empty": "There are no Streams", - "add": "Add Stream", - "form-title": "{id, select, undefined{New} other{Edit}} Stream", - "incoming-port": "Incoming Port", - "forwarding-host": "Forward Host", - "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.", - "search": "Search Incoming Port…" - }, - "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", - "test-reachability": "Test Server Reachability", - "reachability-title": "Test Server Reachability", - "reachability-info": "Test whether the domains are reachable from the public internet using Site24x7. This is not necessary when using the DNS Challenge.", - "reachability-failed-to-reach-api": "Communication with the API failed, is NPM running correctly?", - "reachability-failed-to-check": "Failed to check the reachability due to a communication error with site24x7.com.", - "reachability-ok": "Your server is reachable and creating certificates should be possible.", - "reachability-404": "There is a server found at this domain but it does not seem to be Nginx Proxy Manager. Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-not-resolved": "There is no server available at this domain. Please make sure your domain exists and points to the IP where your NPM instance is running and if necessary port 80 is forwarded in your router.", - "reachability-wrong-data": "There is a server found at this domain but it returned an unexpected data. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.", - "download": "Download", - "renew-title": "Renew Let'sEncrypt Certificate", - "search": "Search 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", - "search": "Search Access…" - }, - "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", - "search": "Search User…" - }, - "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", - "search": "Search Log…" - }, - "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 3d817d71d..000000000 --- a/frontend/js/index.js +++ /dev/null @@ -1,119 +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' - } -}; - -String.prototype.toHtmlEntities = function() { - return this.replace(/./gm, function(s) { - // return "&#" + s.charCodeAt(0) + ";"; - return (s.match(/[a-z0-9\s]+/i)) ? s : "&#" + s.charCodeAt(0) + ";"; - }); -}; - -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 21ce74243..000000000 --- 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 c88368f89..000000000 --- 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 0094e2a23..000000000 --- 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 03fdc7e56..000000000 --- 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 b6f52b7a9..000000000 --- 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 757eb4e31..000000000 --- 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 0c2c4abea..000000000 --- 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 c929a0bd8..000000000 --- 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 c7d0b2d97..000000000 --- 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 98ceef29e..000000000 --- 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 2a35059f8..000000000 --- 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 b82d09fef..000000000 --- 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 1d0b0de24..000000000 --- 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 c70a4e9cb..000000000 --- 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 ba035429a..000000000 --- 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, - forwarding_host: 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 a8e4ed9e9..000000000 --- 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 198e6c938..dd6499103 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,47 +1,78 @@ { - "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-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": "^6.0.1", - "nodemon": "^2.0.2", - "numeral": "^2.0.6", - "sass-loader": "10.2.0", - "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": "3.0.0", + "private": true, + "scripts": { + "dev": "vite --host", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:ci": "yarn lint --watchAll=false", + "lint:fix": "eslint --fix --ext .ts --ext .tsx .", + "preview": "vite preview", + "prettier": "prettier \"**/*.+(js|json|yml|css|ts|tsx)\"", + "format": "yarn prettier -- --write", + "locale-extract": "formatjs extract 'src/**/*.tsx'", + "locale-compile": "formatjs compile-folder src/locale/src src/locale/lang", + "check-locales": "./check-locales.js" + }, + "dependencies": { + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@tanstack/react-query": "^5.35.1", + "@tanstack/react-query-devtools": "^5.35.1", + "ajv": "^8.13.0", + "chakra-react-select": "^4.7.6", + "classnames": "^2.5.1", + "country-flag-icons": "^1.5.11", + "date-fns": "3.6.0", + "formik": "^2.4.6", + "framer-motion": "^11.1.9", + "humps": "^2.0.1", + "lodash": "4.17.21", + "moment": "2.30.1", + "query-string": "9.0.0", + "react": "^18.3.1", + "react-async": "10.0.1", + "react-dom": "18.3.1", + "react-focus-lock": "^2.12.1", + "react-icons": "^5.2.1", + "react-intl": "^6.6.6", + "react-markdown": "^9.0.1", + "react-router-dom": "^6.23.0", + "react-syntax-highlighter": "^15.5.0", + "react-table": "7.8.0", + "rooks": "7.14.1", + "tmp": "^0.2.3" + }, + "devDependencies": { + "@formatjs/cli": "^6.2.10", + "@types/country-flag-icons": "^1.2.2", + "@types/humps": "^2.0.6", + "@types/lodash": "4.17.1", + "@types/node": "20.12.10", + "@types/react": "18.3.1", + "@types/react-dom": "18.3.0", + "@types/react-router-dom": "5.3.3", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/react-table": "^7.7.20", + "@types/styled-components": "5.1.34", + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.45.0", + "eslint-config-prettier": "^8.8.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "prettier": "^3.2.5", + "sass": "^1.77.0", + "typescript": "^5.4.5", + "vite": "^5.2.11", + "vite-plugin-checker": "^0.6.4" + } } diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff new file mode 100644 index 000000000..f2a7dd342 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff2 b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff2 new file mode 100644 index 000000000..ce34a9fec Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff2 differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff new file mode 100644 index 000000000..38faafb98 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff2 b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff2 new file mode 100644 index 000000000..a63c72797 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff2 differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff new file mode 100644 index 000000000..4e767cf5c Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff2 b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff2 new file mode 100644 index 000000000..d3e979ba6 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff2 differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff new file mode 100644 index 000000000..5b6e97be7 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff differ diff --git a/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff2 b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff2 new file mode 100644 index 000000000..36bdc0e32 Binary files /dev/null and b/frontend/public/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff2 differ 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 000000000..c87396f6a --- /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-no-text.svg b/frontend/public/images/logo-no-text.svg new file mode 100644 index 000000000..dc3c1163c --- /dev/null +++ b/frontend/public/images/logo-no-text.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 000000000..057a83d1b 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/scss/custom.scss b/frontend/scss/custom.scss deleted file mode 100644 index 4037dcf6c..000000000 --- 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 f0ec1b739..000000000 --- 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 e12d5b694..000000000 --- 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 527330972..000000000 --- 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 3ddd0ed4d..000000000 --- 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.tsx b/frontend/src/App.tsx new file mode 100644 index 000000000..5eed11969 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,32 @@ +import { ChakraProvider } from "@chakra-ui/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { RawIntlProvider } from "react-intl"; + +import { AuthProvider, LocaleProvider } from "src/context"; +import { intl } from "src/locale"; + +import Router from "./Router"; +import lightTheme from "./theme/customTheme"; + +// Create a client +const queryClient = new QueryClient(); + +function App() { + return ( + + + + + + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx new file mode 100644 index 000000000..5d3616ba7 --- /dev/null +++ b/frontend/src/Router.tsx @@ -0,0 +1,96 @@ +import { lazy, Suspense } from "react"; + +import { BrowserRouter, Route, Routes } from "react-router-dom"; + +import { TokenResponse } from "src/api/npm"; +import { SiteWrapper, SpinnerPage, Unhealthy } from "src/components"; +import { useAuthState, useLocaleState } from "src/context"; +import { useHealth } from "src/hooks"; + +const AccessLists = lazy(() => import("src/pages/AccessLists")); +const AuditLog = lazy(() => import("src/pages/AuditLog")); +const Certificates = lazy(() => import("src/pages/Certificates")); +const CertificateAuthorities = lazy( + () => import("src/pages/CertificateAuthorities"), +); +const Dashboard = lazy(() => import("src/pages/Dashboard")); +const DNSProviders = lazy(() => import("src/pages/DNSProviders")); +const Hosts = lazy(() => import("src/pages/Hosts")); +const NginxTemplates = lazy(() => import("src/pages/NginxTemplates")); +const Login = lazy(() => import("src/pages/Login")); +const GeneralSettings = lazy(() => import("src/pages/Settings")); +const Setup = lazy(() => import("src/pages/Setup")); +const Upstreams = lazy(() => import("src/pages/Upstreams")); +const Users = lazy(() => import("src/pages/Users")); + +function Router() { + const health = useHealth(); + const { authenticated, handleTokenUpdate } = useAuthState(); + const { locale } = useLocaleState(); + const Spinner = ; + + // Load token from URL Query Params + const searchParams = new URLSearchParams(document.location.search); + const t = searchParams.get("token_response"); + if (t) { + const tokenResponse: TokenResponse = JSON.parse(t); + handleTokenUpdate(tokenResponse); + window.location.href = "/"; + return; + } + // End Load token from URL Query Params + + if (health.isLoading) { + return Spinner; + } + + if (health.isError || !health.data?.healthy) { + return ; + } + + if (health.data?.healthy && !health.data?.setup) { + return ( + + + + ); + } + + if (!authenticated) { + return ( + + + + ); + } + + return ( + + + + + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + } + /> + } /> + } /> + + + + + ); +} + +export default Router; diff --git a/frontend/src/api/npm/base.ts b/frontend/src/api/npm/base.ts new file mode 100644 index 000000000..a9bcc86df --- /dev/null +++ b/frontend/src/api/npm/base.ts @@ -0,0 +1,119 @@ +import { camelizeKeys, decamelizeKeys } from "humps"; +import queryString from "query-string"; + +import AuthStore from "src/modules/AuthStore"; + +const contentTypeHeader = "Content-Type"; + +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, skipDecamelize = false) { + if (data) { + return JSON.stringify(skipDecamelize ? data : decamelizeKeys(data)); + } +} + +async function processResponse(response: Response, skipCamelize = false) { + const payload = await response.json(); + if (!response.ok) { + throw new Error(payload.error.message); + } + return (skipCamelize ? payload : camelizeKeys(payload)) as any; +} + +interface GetArgs { + url: string; + params?: queryString.StringifiableRecord; + skipCamelize?: boolean; +} + +export async function get( + { url, params, skipCamelize }: 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, skipCamelize); +} + +interface PostArgs { + url: string; + data?: any; + skipCamelize?: boolean; + skipDecamelize?: boolean; +} + +export async function post( + { url, data, skipCamelize, skipDecamelize }: PostArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "POST"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const body = buildBody(data, skipDecamelize); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response, skipCamelize); +} + +interface PutArgs { + url: string; + data?: any; + skipCamelize?: boolean; + skipDecamelize?: boolean; +} +export async function put( + { url, data, skipCamelize, skipDecamelize }: PutArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "PUT"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const body = buildBody(data, skipDecamelize); + const response = await fetch(apiUrl, { method, headers, body, signal }); + return processResponse(response, skipCamelize); +} + +interface DeleteArgs { + url: string; +} +export async function del( + { url }: DeleteArgs, + abortController?: AbortController, +) { + const apiUrl = buildUrl({ url }); + const method = "DELETE"; + const headers = { + ...buildAuthHeader(), + [contentTypeHeader]: "application/json", + }; + const signal = abortController?.signal; + const response = await fetch(apiUrl, { method, headers, signal }); + return processResponse(response); +} diff --git a/frontend/src/api/npm/createCertificate.ts b/frontend/src/api/npm/createCertificate.ts new file mode 100644 index 000000000..5f662a634 --- /dev/null +++ b/frontend/src/api/npm/createCertificate.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { Certificate } from "./models"; + +export async function createCertificate( + data: Certificate, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/certificates", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/createCertificateAuthority.ts b/frontend/src/api/npm/createCertificateAuthority.ts new file mode 100644 index 000000000..1822c7445 --- /dev/null +++ b/frontend/src/api/npm/createCertificateAuthority.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function createCertificateAuthority( + data: CertificateAuthority, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/certificate-authorities", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/createDNSProvider.ts b/frontend/src/api/npm/createDNSProvider.ts new file mode 100644 index 000000000..fea8a5b99 --- /dev/null +++ b/frontend/src/api/npm/createDNSProvider.ts @@ -0,0 +1,27 @@ +import { decamelizeKeys } from "humps"; + +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function createDNSProvider( + data: DNSProvider, + abortController?: AbortController, +): Promise { + // Because the meta property of the data should not be decamelized, + // we're going to decamelize the rest here instead of in base.ts + const dcData: any = decamelizeKeys(data); + if (typeof data.meta !== "undefined") { + dcData.meta = data.meta; + } + + const { result } = await api.post( + { + url: "/dns-providers", + data: dcData, + skipCamelize: true, + skipDecamelize: true, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/createUser.ts b/frontend/src/api/npm/createUser.ts new file mode 100644 index 000000000..59f740359 --- /dev/null +++ b/frontend/src/api/npm/createUser.ts @@ -0,0 +1,29 @@ +import * as api from "./base"; +import { User } from "./models"; + +export interface AuthOptions { + type: string; + secret: string; +} + +export interface NewUser { + name: string; + email: string; + isDisabled: boolean; + auth: AuthOptions; + capabilities: string[]; +} + +export async function createUser( + data: NewUser, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/users", + data, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/deleteCertificate.ts b/frontend/src/api/npm/deleteCertificate.ts new file mode 100644 index 000000000..75afabe97 --- /dev/null +++ b/frontend/src/api/npm/deleteCertificate.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; + +export async function deleteCertificate( + id: number, + abortController?: AbortController, +): Promise { + const { result } = await api.del( + { + url: `/certificates/${id}`, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getAccessLists.ts b/frontend/src/api/npm/getAccessLists.ts new file mode 100644 index 000000000..7fb846594 --- /dev/null +++ b/frontend/src/api/npm/getAccessLists.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { AccessListsResponse } from "./responseTypes"; + +export async function getAccessLists( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "access-lists", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getCertificate.ts b/frontend/src/api/npm/getCertificate.ts new file mode 100644 index 000000000..32cc84d91 --- /dev/null +++ b/frontend/src/api/npm/getCertificate.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import { Certificate } from "./models"; + +export async function getCertificate( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/certificates/${id}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getCertificateAuthorities.ts b/frontend/src/api/npm/getCertificateAuthorities.ts new file mode 100644 index 000000000..e956bf529 --- /dev/null +++ b/frontend/src/api/npm/getCertificateAuthorities.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { CertificateAuthoritiesResponse } from "./responseTypes"; + +export async function getCertificateAuthorities( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "certificate-authorities", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getCertificateAuthority.ts b/frontend/src/api/npm/getCertificateAuthority.ts new file mode 100644 index 000000000..10674f14a --- /dev/null +++ b/frontend/src/api/npm/getCertificateAuthority.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function getCertificateAuthority( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/certificate-authorities/${id}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getCertificates.ts b/frontend/src/api/npm/getCertificates.ts new file mode 100644 index 000000000..c8df51f48 --- /dev/null +++ b/frontend/src/api/npm/getCertificates.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { CertificatesResponse } from "./responseTypes"; + +export async function getCertificates( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "certificates", + params: { limit, offset, sort, expand: "user", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getDNSProvider.ts b/frontend/src/api/npm/getDNSProvider.ts new file mode 100644 index 000000000..380e5d786 --- /dev/null +++ b/frontend/src/api/npm/getDNSProvider.ts @@ -0,0 +1,13 @@ +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function getDNSProvider( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/dns-providers/${id}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getDNSProviders.ts b/frontend/src/api/npm/getDNSProviders.ts new file mode 100644 index 000000000..51028a566 --- /dev/null +++ b/frontend/src/api/npm/getDNSProviders.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { DNSProvidersResponse } from "./responseTypes"; + +export async function getDNSProviders( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "dns-providers", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getDNSProvidersAcmesh.ts b/frontend/src/api/npm/getDNSProvidersAcmesh.ts new file mode 100644 index 000000000..b46d55f9a --- /dev/null +++ b/frontend/src/api/npm/getDNSProvidersAcmesh.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { DNSProvidersAcmesh } from "./models"; + +export async function getDNSProvidersAcmesh( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "dns-providers/acmesh", + // Important for this endpoint: + skipCamelize: true, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getHealth.ts b/frontend/src/api/npm/getHealth.ts new file mode 100644 index 000000000..9c6d77b91 --- /dev/null +++ b/frontend/src/api/npm/getHealth.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { HealthResponse } from "./responseTypes"; + +export async function getHealth( + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getHosts.ts b/frontend/src/api/npm/getHosts.ts new file mode 100644 index 000000000..4529fb8e3 --- /dev/null +++ b/frontend/src/api/npm/getHosts.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { HostsResponse } from "./responseTypes"; + +export async function getHosts( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "hosts", + params: { limit, offset, sort, expand: "user,certificate", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getNginxTemplates.ts b/frontend/src/api/npm/getNginxTemplates.ts new file mode 100644 index 000000000..15d8e2873 --- /dev/null +++ b/frontend/src/api/npm/getNginxTemplates.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { NginxTemplatesResponse } from "./responseTypes"; + +export async function getNginxTemplates( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "nginx-templates", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getSSEToken.ts b/frontend/src/api/npm/getSSEToken.ts new file mode 100644 index 000000000..c8a2a602f --- /dev/null +++ b/frontend/src/api/npm/getSSEToken.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { TokenResponse } from "./responseTypes"; + +export async function getSSEToken( + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: "/auth/sse", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getSettings.ts b/frontend/src/api/npm/getSettings.ts new file mode 100644 index 000000000..6e07806d9 --- /dev/null +++ b/frontend/src/api/npm/getSettings.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { SettingsResponse } from "./responseTypes"; + +export async function getSettings( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "settings", + params: { limit, offset, sort, ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getToken.ts b/frontend/src/api/npm/getToken.ts new file mode 100644 index 000000000..750016ec6 --- /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: "/auth", + data: payload, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getUpstreamNginxConfig.ts b/frontend/src/api/npm/getUpstreamNginxConfig.ts new file mode 100644 index 000000000..f106344d4 --- /dev/null +++ b/frontend/src/api/npm/getUpstreamNginxConfig.ts @@ -0,0 +1,12 @@ +import * as api from "./base"; + +export async function getUpstreamNginxConfig( + id: number, + params = {}, +): Promise { + const { result } = await api.get({ + url: `/upstreams/${id}/nginx-config`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getUpstreams.ts b/frontend/src/api/npm/getUpstreams.ts new file mode 100644 index 000000000..e64c29e9d --- /dev/null +++ b/frontend/src/api/npm/getUpstreams.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { UpstreamsResponse } from "./responseTypes"; + +export async function getUpstreams( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "upstreams", + params: { limit, offset, sort, expand: "user", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/getUser.ts b/frontend/src/api/npm/getUser.ts new file mode 100644 index 000000000..a6485f915 --- /dev/null +++ b/frontend/src/api/npm/getUser.ts @@ -0,0 +1,14 @@ +import * as api from "./base"; +import { User } from "./models"; + +export async function getUser( + id: number | string = "me", + params = {}, +): Promise { + const userId = id ? id : "me"; + const { result } = await api.get({ + url: `/users/${userId}`, + params, + }); + return result; +} diff --git a/frontend/src/api/npm/getUsers.ts b/frontend/src/api/npm/getUsers.ts new file mode 100644 index 000000000..89b163d5e --- /dev/null +++ b/frontend/src/api/npm/getUsers.ts @@ -0,0 +1,19 @@ +import * as api from "./base"; +import { UsersResponse } from "./responseTypes"; + +export async function getUsers( + offset = 0, + limit = 10, + sort?: string, + filters?: { [key: string]: string }, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "users", + params: { limit, offset, sort, expand: "capabilities", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/helpers.ts b/frontend/src/api/npm/helpers.ts new file mode 100644 index 000000000..da6b8ca89 --- /dev/null +++ b/frontend/src/api/npm/helpers.ts @@ -0,0 +1,34 @@ +import { decamelize } from "humps"; + +/** + * This will convert a react-table sort object into + * a string that the backend api likes: + * name.asc,id.desc + */ +export function tableSortToAPI(sortBy: any): string | undefined { + if (sortBy?.length > 0) { + const strs: string[] = []; + sortBy.map((item: any) => { + strs.push(decamelize(item.id) + "." + (item.desc ? "desc" : "asc")); + return undefined; + }); + return strs.join(","); + } + return; +} + +/** + * This will convert a react-table filters object into + * a string that the backend api likes: + * name:contains=jam + */ +export function tableFiltersToAPI(filters: any): { [key: string]: string } { + const items: { [key: string]: string } = {}; + if (filters?.length > 0) { + filters.map((item: any) => { + items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value; + return undefined; + }); + } + return items; +} diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts new file mode 100644 index 000000000..d554c0038 --- /dev/null +++ b/frontend/src/api/npm/index.ts @@ -0,0 +1,33 @@ +export * from "./createCertificate"; +export * from "./createCertificateAuthority"; +export * from "./createDNSProvider"; +export * from "./createUser"; +export * from "./deleteCertificate"; +export * from "./getAccessLists"; +export * from "./getCertificate"; +export * from "./getCertificateAuthorities"; +export * from "./getCertificateAuthority"; +export * from "./getCertificates"; +export * from "./getDNSProvider"; +export * from "./getDNSProviders"; +export * from "./getDNSProvidersAcmesh"; +export * from "./getHealth"; +export * from "./getHosts"; +export * from "./getNginxTemplates"; +export * from "./getSettings"; +export * from "./getSSEToken"; +export * from "./getToken"; +export * from "./getUpstreamNginxConfig"; +export * from "./getUpstreams"; +export * from "./getUser"; +export * from "./getUsers"; +export * from "./helpers"; +export * from "./models"; +export * from "./refreshToken"; +export * from "./renewCertificate"; +export * from "./responseTypes"; +export * from "./setAuth"; +export * from "./setCertificate"; +export * from "./setCertificateAuthority"; +export * from "./setDNSProvider"; +export * from "./setUser"; diff --git a/frontend/src/api/npm/models.ts b/frontend/src/api/npm/models.ts new file mode 100644 index 000000000..75e86089c --- /dev/null +++ b/frontend/src/api/npm/models.ts @@ -0,0 +1,173 @@ +export interface Sort { + field: string; + direction: "ASC" | "DESC"; +} + +export interface UserAuth { + id: number; + userId: number; + type: string; + createdOn: number; + updatedOn: number; +} + +export interface User { + id: number; + name: string; + email: string; + createdOn: number; + updatedOn: number; + gravatarUrl: string; + isDisabled: boolean; + notifications: Notification[]; + capabilities?: string[]; + auth?: UserAuth; +} + +export interface Notification { + title: string; + seen: boolean; +} + +export interface Setting { + id: number; + createdOn: number; + modifiedOn: number; + name: string; + value: any; +} + +export interface AccessList { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + name: string; + meta: any; +} + +// TODO: copy pasta not right +export interface Certificate { + id: number; + createdOn: number; + modifiedOn: number; + expiresOn: number | null; + type: string; + userId: number; + certificateAuthorityId: number; + dnsProviderId: number; + name: string; + domainNames: string[]; + status: string; + errorMessage: string; + isEcc: boolean; +} + +export interface CertificateAuthority { + id: number; + createdOn: number; + modifiedOn: number; + name: string; + acmeshServer: string; + caBundle: string; + maxDomains: number; + isWildcardSupported: boolean; + isReadonly: boolean; +} + +export interface DNSProvider { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + name: string; + acmeshName: string; + dnsSleep: number; + meta: any; +} + +export interface DNSProvidersAcmeshProperty { + title: string; + type: string; + additionalProperties: boolean; + minimum: number; + maximum: number; + minLength: number; + maxLength: number; + pattern: string; + isSecret: boolean; +} + +export interface DNSProvidersAcmesh { + title: string; + type: string; + additionalProperties: boolean; + minProperties: number; + required: string[]; + properties: any; +} + +export interface Host { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + type: string; + nginxTemplateId: number; + listenInterface: number; + domainNames: string[]; + upstreamId: number; + certificateId: number; + accessListId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + paths: string; + advancedConfig: string; + isDisabled: boolean; +} + +export interface NginxTemplate { + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + type: string; + template: string; +} + +export interface Upstream { + // todo + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + type: string; + nginxTemplateId: number; + listenInterface: number; + domainNames: string[]; + upstreamId: number; + certificateId: number; + accessListId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + paths: string; + advancedConfig: string; + isDisabled: boolean; +} + +export interface SSEMessage { + lang?: string; + langParams?: string; + type?: "info" | "warning" | "success" | "error" | "loading"; + affects?: string | string[]; +} diff --git a/frontend/src/api/npm/refreshToken.ts b/frontend/src/api/npm/refreshToken.ts new file mode 100644 index 000000000..7f448f565 --- /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.post( + { + url: "/auth/refresh", + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/renewCertificate.ts b/frontend/src/api/npm/renewCertificate.ts new file mode 100644 index 000000000..a7b513c44 --- /dev/null +++ b/frontend/src/api/npm/renewCertificate.ts @@ -0,0 +1,15 @@ +import * as api from "./base"; +import { Certificate } from "./models"; + +export async function renewCertificate( + id: number, + abortController?: AbortController, +): Promise { + const { result } = await api.post( + { + url: `/certificates/${id}/renew`, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts new file mode 100644 index 000000000..0135a12b6 --- /dev/null +++ b/frontend/src/api/npm/responseTypes.ts @@ -0,0 +1,68 @@ +import { + AccessList, + Certificate, + CertificateAuthority, + DNSProvider, + Host, + NginxTemplate, + Setting, + Sort, + User, + Upstream, +} from "./models"; + +export interface BaseResponse { + total: number; + offset: number; + limit: number; + sort: Sort[]; +} + +export interface HealthResponse { + commit: string; + errorReporting: boolean; + healthy: boolean; + setup: boolean; + version: string; +} + +export interface TokenResponse { + expires: number; + token: string; +} + +export interface SettingsResponse extends BaseResponse { + items: Setting[]; +} + +export interface AccessListsResponse extends BaseResponse { + items: AccessList[]; +} + +export interface CertificatesResponse extends BaseResponse { + items: Certificate[]; +} + +export interface CertificateAuthoritiesResponse extends BaseResponse { + items: CertificateAuthority[]; +} + +export interface UsersResponse extends BaseResponse { + items: User[]; +} + +export interface DNSProvidersResponse extends BaseResponse { + items: DNSProvider[]; +} + +export interface HostsResponse extends BaseResponse { + items: Host[]; +} + +export interface NginxTemplatesResponse extends BaseResponse { + items: NginxTemplate[]; +} + +export interface UpstreamsResponse extends BaseResponse { + items: Upstream[]; +} diff --git a/frontend/src/api/npm/setAuth.ts b/frontend/src/api/npm/setAuth.ts new file mode 100644 index 000000000..f0a7049e0 --- /dev/null +++ b/frontend/src/api/npm/setAuth.ts @@ -0,0 +1,18 @@ +import * as api from "./base"; +import { UserAuth } from "./models"; + +export async function setAuth( + id: number | string = "me", + data: any, +): Promise { + const userId = id ? id : "me"; + if (data.id) { + delete data.id; + } + + const { result } = await api.post({ + url: `/users/${userId}/auth`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setCertificate.ts b/frontend/src/api/npm/setCertificate.ts new file mode 100644 index 000000000..b5bb998cb --- /dev/null +++ b/frontend/src/api/npm/setCertificate.ts @@ -0,0 +1,17 @@ +import * as api from "./base"; +import { Certificate } from "./models"; + +export async function setCertificate( + id: number, + data: any, +): Promise { + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/certificates/${id}`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setCertificateAuthority.ts b/frontend/src/api/npm/setCertificateAuthority.ts new file mode 100644 index 000000000..1719e2edd --- /dev/null +++ b/frontend/src/api/npm/setCertificateAuthority.ts @@ -0,0 +1,17 @@ +import * as api from "./base"; +import { CertificateAuthority } from "./models"; + +export async function setCertificateAuthority( + id: number, + data: any, +): Promise { + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/certificate-authorities/${id}`, + data, + }); + return result; +} diff --git a/frontend/src/api/npm/setDNSProvider.ts b/frontend/src/api/npm/setDNSProvider.ts new file mode 100644 index 000000000..5ae93a91c --- /dev/null +++ b/frontend/src/api/npm/setDNSProvider.ts @@ -0,0 +1,28 @@ +import { decamelizeKeys } from "humps"; + +import * as api from "./base"; +import { DNSProvider } from "./models"; + +export async function setDNSProvider( + id: number, + data: any, +): Promise { + if (data.id) { + delete data.id; + } + + // Because the meta property of the data should not be decamelized, + // we're going to decamelize the rest here instead of in base.ts + const dcData: any = decamelizeKeys(data); + if (typeof data.meta !== "undefined") { + dcData.meta = data.meta; + } + + const { result } = await api.put({ + url: `/dns-providers/${id}`, + data: dcData, + skipCamelize: true, + skipDecamelize: true, + }); + return result; +} diff --git a/frontend/src/api/npm/setUser.ts b/frontend/src/api/npm/setUser.ts new file mode 100644 index 000000000..743dba7a9 --- /dev/null +++ b/frontend/src/api/npm/setUser.ts @@ -0,0 +1,18 @@ +import * as api from "./base"; +import { User } from "./models"; + +export async function setUser( + id: number | string = "me", + data: any, +): Promise { + const userId = id ? id : "me"; + if (data.id) { + delete data.id; + } + + const { result } = await api.put({ + url: `/users/${userId}`, + data, + }); + return result; +} diff --git a/frontend/src/assets/logo-256.png b/frontend/src/assets/logo-256.png new file mode 100644 index 000000000..2bfb661dc Binary files /dev/null and b/frontend/src/assets/logo-256.png differ diff --git a/frontend/src/assets/logo-text-vertical-grey.png b/frontend/src/assets/logo-text-vertical-grey.png new file mode 100644 index 000000000..70273171a Binary files /dev/null and b/frontend/src/assets/logo-text-vertical-grey.png differ diff --git a/frontend/src/components/EmptyList.tsx b/frontend/src/components/EmptyList.tsx new file mode 100644 index 000000000..2f6a83b58 --- /dev/null +++ b/frontend/src/components/EmptyList.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from "react"; + +import { Box, Heading, Text } from "@chakra-ui/react"; + +interface EmptyListProps { + title: string; + summary: string; + createButton?: ReactNode; +} + +function EmptyList({ title, summary, createButton }: EmptyListProps) { + return ( + + + {title} + + {summary} + {createButton} + + ); +} + +export { EmptyList }; diff --git a/frontend/src/components/Flag/Flag.tsx b/frontend/src/components/Flag/Flag.tsx new file mode 100644 index 000000000..6f264f791 --- /dev/null +++ b/frontend/src/components/Flag/Flag.tsx @@ -0,0 +1,32 @@ +import { Box } from "@chakra-ui/layout"; +import { hasFlag } from "country-flag-icons"; +// @ts-ignore Creating a typing for a subfolder is not easily possible +import Flags from "country-flag-icons/react/3x2"; + +interface FlagProps { + /** + * Additional Class + */ + className?: string; + /** + * Two letter country code of flag + */ + countryCode: string; +} +function Flag({ className, countryCode }: FlagProps) { + countryCode = countryCode.toUpperCase(); + + if (hasFlag(countryCode)) { + // @ts-ignore have to do this because of above + const FlagElement = Flags[countryCode] as any; + return ( + + ); + } else { + console.error(`No flag for country ${countryCode} found!`); + + return ; + } +} + +export { Flag }; diff --git a/frontend/src/components/Flag/index.ts b/frontend/src/components/Flag/index.ts new file mode 100644 index 000000000..62845cc19 --- /dev/null +++ b/frontend/src/components/Flag/index.ts @@ -0,0 +1 @@ +export * from "./Flag"; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx new file mode 100644 index 000000000..d4c21174c --- /dev/null +++ b/frontend/src/components/Footer.tsx @@ -0,0 +1,65 @@ +import { + Box, + Container, + Link, + Stack, + Text, + Tooltip, + useColorModeValue, +} from "@chakra-ui/react"; + +import { intl } from "src/locale"; + +function Footer() { + return ( + + + + {intl.formatMessage( + { id: "footer.copyright" }, + { year: new Date().getFullYear() }, + )} + + + + {intl.formatMessage({ id: "footer.userguide" })} + + + {intl.formatMessage({ id: "footer.changelog" })} + + + {intl.formatMessage({ id: "footer.github" })} + + + + v{import.meta.env.VITE_APP_VERSION} + + + + + + ); +} + +export { Footer }; diff --git a/frontend/src/components/HelpDrawer/HelpDrawer.tsx b/frontend/src/components/HelpDrawer/HelpDrawer.tsx new file mode 100644 index 000000000..1c935d2e5 --- /dev/null +++ b/frontend/src/components/HelpDrawer/HelpDrawer.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; + +import { + Button, + Drawer, + DrawerContent, + DrawerOverlay, + DrawerBody, + useDisclosure, +} from "@chakra-ui/react"; +import { FiHelpCircle } from "react-icons/fi"; +import ReactMarkdown from "react-markdown"; + +import { getLocale } from "src/locale"; +import { getHelpFile } from "src/locale/src/HelpDoc"; + +interface HelpDrawerProps { + /** + * Section to show + */ + section: string; +} +function HelpDrawer({ section }: HelpDrawerProps) { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [markdownText, setMarkdownText] = useState(""); + const lang = getLocale(true); + + useEffect(() => { + try { + const docFile = getHelpFile(lang, section) as any; + fetch(docFile) + .then((response) => response.text()) + .then(setMarkdownText); + } catch (ex: any) { + setMarkdownText(`**ERROR:** ${ex.message}`); + } + }, [lang, section]); + + return ( + <> + + + + + + {markdownText} + + + + + ); +} + +export { HelpDrawer }; diff --git a/frontend/src/components/HelpDrawer/index.ts b/frontend/src/components/HelpDrawer/index.ts new file mode 100644 index 000000000..4b6357df4 --- /dev/null +++ b/frontend/src/components/HelpDrawer/index.ts @@ -0,0 +1 @@ +export * from "./HelpDrawer"; diff --git a/frontend/src/components/Loader/Loader.tsx b/frontend/src/components/Loader/Loader.tsx new file mode 100644 index 000000000..2c44bd6ca --- /dev/null +++ b/frontend/src/components/Loader/Loader.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; + +import cn from "classnames"; + +interface LoaderProps { + /** + * Child elements within + */ + children?: ReactNode; + /** + * Additional Class + */ + className?: string; +} +function Loader({ children, className }: LoaderProps) { + return
{children}
; +} + +export { Loader }; diff --git a/frontend/src/components/Loader/index.ts b/frontend/src/components/Loader/index.ts new file mode 100644 index 000000000..f9f5a2b49 --- /dev/null +++ b/frontend/src/components/Loader/index.ts @@ -0,0 +1 @@ +export * from "./Loader"; diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx new file mode 100644 index 000000000..baf05c4f5 --- /dev/null +++ b/frontend/src/components/Loading.tsx @@ -0,0 +1,13 @@ +import { Box } from "@chakra-ui/react"; + +import { Loader } from "src/components"; + +function Loading() { + return ( + + + + ); +} + +export { Loading }; diff --git a/frontend/src/components/LocalePicker.tsx b/frontend/src/components/LocalePicker.tsx new file mode 100644 index 000000000..225840738 --- /dev/null +++ b/frontend/src/components/LocalePicker.tsx @@ -0,0 +1,64 @@ +import { + Button, + Box, + Menu, + MenuButton, + MenuButtonProps, + MenuList, + MenuItem, +} from "@chakra-ui/react"; + +import { Flag } from "src/components"; +import { useLocaleState } from "src/context"; +import { + changeLocale, + getFlagCodeForLocale, + intl, + localeOptions, +} from "src/locale"; + +interface LocalPickerProps { + onChange?: any; + className?: string; + background?: "normal" | "transparent"; +} + +function LocalePicker({ onChange, className, background }: LocalPickerProps) { + const { locale, setLocale } = useLocaleState(); + + const additionalProps: Partial = {}; + if (background === "transparent") { + additionalProps["backgroundColor"] = "transparent"; + } + + const changeTo = (lang: string) => { + changeLocale(lang); + setLocale(lang); + onChange && onChange(locale); + location.reload(); + }; + + return ( + + + + + + + {localeOptions.map((item) => { + return ( + } + onClick={() => changeTo(item[0])} + key={`locale-${item[0]}`}> + {intl.formatMessage({ id: `locale-${item[1]}` })} + + ); + })} + + + + ); +} + +export { LocalePicker }; diff --git a/frontend/src/components/Monospace.tsx b/frontend/src/components/Monospace.tsx new file mode 100644 index 000000000..0f9dffcd6 --- /dev/null +++ b/frontend/src/components/Monospace.tsx @@ -0,0 +1,11 @@ +import { Text, TextProps } from "@chakra-ui/react"; + +function Monospace(props: TextProps) { + return ( + + {props.children} + + ); +} + +export { Monospace }; diff --git a/frontend/src/components/Navigation/Navigation.tsx b/frontend/src/components/Navigation/Navigation.tsx new file mode 100644 index 000000000..9f8dea26d --- /dev/null +++ b/frontend/src/components/Navigation/Navigation.tsx @@ -0,0 +1,25 @@ +import { useDisclosure } from "@chakra-ui/react"; + +import { NavigationHeader, NavigationMenu } from "src/components"; + +function Navigation() { + const { + isOpen: mobileNavIsOpen, + onToggle: mobileNavToggle, + onClose: mobileNavClose, + } = useDisclosure(); + return ( + <> + + + + ); +} + +export { Navigation }; diff --git a/frontend/src/components/Navigation/NavigationHeader.tsx b/frontend/src/components/Navigation/NavigationHeader.tsx new file mode 100644 index 000000000..498e10a87 --- /dev/null +++ b/frontend/src/components/Navigation/NavigationHeader.tsx @@ -0,0 +1,117 @@ +import { + Avatar, + Box, + Button, + chakra, + Container, + Flex, + HStack, + Icon, + IconButton, + Menu, + MenuButton, + MenuDivider, + MenuItem, + MenuList, + Text, + useColorModeValue, + useDisclosure, +} from "@chakra-ui/react"; +import { FiLock, FiLogOut, FiMenu, FiUser, FiX } from "react-icons/fi"; + +import { LocalePicker, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { useUser } from "src/hooks"; +import { intl } from "src/locale"; +import { ChangePasswordModal, ProfileModal } from "src/modals"; + +interface NavigationHeaderProps { + mobileNavIsOpen: boolean; + toggleMobileNav: () => void; +} +function NavigationHeader({ + mobileNavIsOpen, + toggleMobileNav, +}: NavigationHeaderProps) { + const passwordDisclosure = useDisclosure(); + const profileDisclosure = useDisclosure(); + const { data: user } = useUser("me"); + const { logout } = useAuthState(); + + return ( + + + + } + /> + + + + {intl.formatMessage({ id: "brand.name" })} + + + + + + + + + + + + } + onClick={profileDisclosure.onOpen}> + {intl.formatMessage({ id: "profile.title" })} + + } + onClick={passwordDisclosure.onOpen}> + {intl.formatMessage({ id: "change-password" })} + + + }> + {intl.formatMessage({ id: "profile.logout" })} + + + + + + + + + + + ); +} + +export { NavigationHeader }; diff --git a/frontend/src/components/Navigation/NavigationMenu.tsx b/frontend/src/components/Navigation/NavigationMenu.tsx new file mode 100644 index 000000000..ef7ce0275 --- /dev/null +++ b/frontend/src/components/Navigation/NavigationMenu.tsx @@ -0,0 +1,340 @@ +import { FC, useCallback, useMemo, ReactNode } from "react"; + +import { + Box, + Collapse, + Flex, + forwardRef, + HStack, + Icon, + Link, + Menu, + MenuButton, + MenuItem, + MenuList, + Text, + Stack, + useColorModeValue, + useDisclosure, + Container, + useBreakpointValue, +} from "@chakra-ui/react"; +import { + FiHome, + FiSettings, + FiUser, + FiBook, + FiLock, + FiShield, + FiMonitor, + FiChevronDown, +} from "react-icons/fi"; +import { Link as RouterLink, useLocation } from "react-router-dom"; + +import { intl } from "src/locale"; + +interface NavItem { + /** Displayed label */ + label: string; + /** Icon shown before the label */ + icon: ReactNode; + /** Link where to navigate to */ + to?: string; + subItems?: { label: string; to: string }[]; +} + +const navItems: NavItem[] = [ + { + label: intl.formatMessage({ id: "dashboard.title" }), + icon: , + to: "/", + }, + { + label: intl.formatMessage({ id: "hosts.title" }), + icon: , + subItems: [ + { + label: intl.formatMessage({ id: "hosts.title" }), + to: "/hosts", + }, + { + label: intl.formatMessage({ id: "upstreams.title" }), + to: "/upstreams", + }, + ], + }, + { + label: intl.formatMessage({ id: "access-lists.title" }), + icon: , + to: "/access-lists", + }, + { + label: intl.formatMessage({ id: "ssl.title" }), + icon: , + subItems: [ + { + label: intl.formatMessage({ id: "certificates.title" }), + to: "/ssl/certificates", + }, + { + label: intl.formatMessage({ id: "certificate-authorities.title" }), + to: "/ssl/authorities", + }, + { + label: intl.formatMessage({ id: "dns-providers.title" }), + to: "/ssl/dns-providers", + }, + ], + }, + { + label: intl.formatMessage({ id: "audit-log.title" }), + icon: , + to: "/audit-log", + }, + { + label: intl.formatMessage({ id: "users.title" }), + icon: , + to: "/users", + }, + { + label: intl.formatMessage({ id: "settings.title" }), + icon: , + subItems: [ + { + label: intl.formatMessage({ id: "general-settings.title" }), + to: "/settings/general", + }, + { + label: intl.formatMessage({ id: "nginx-templates.title" }), + to: "/settings/nginx-templates", + }, + ], + }, +]; + +interface NavigationMenuProps { + /** Navigation is currently hidden on mobile */ + mobileNavIsOpen: boolean; + closeMobileNav: () => void; +} +function NavigationMenu({ + mobileNavIsOpen, + closeMobileNav, +}: NavigationMenuProps) { + const isMobile = useBreakpointValue({ base: true, md: false }); + return ( + <> + {isMobile ? ( + + + + ) : ( + + )} + + ); +} + +/** Single tab element for desktop navigation */ +type NavTabProps = Omit & { active?: boolean }; +const NavTab = forwardRef( + ({ label, icon, to, active, ...props }, ref) => { + const linkColor = useColorModeValue("gray.500", "gray.200"); + const linkHoverColor = useColorModeValue("gray.900", "white"); + return ( + + {icon} + + {label} + + + ); + }, +); + +const DesktopNavigation: FC = () => { + const path = useLocation().pathname; + const activeNavItemIndex = useMemo( + () => + navItems.findIndex((item) => { + // Find the nav item whose location / sub items location is the beginning of the currently active path + if (item.to) { + if (item.to === "/") { + return path === item.to; + } + return path.startsWith(item.to !== "" ? item.to : "/dashboard"); + } else if (item.subItems) { + return item.subItems.some((subItem) => path.startsWith(subItem.to)); + } + return false; + }), + [path], + ); + + return ( + + + + {navItems.map((navItem, index) => { + const { subItems, ...propsWithoutSubItems } = navItem; + const additionalProps: Partial = {}; + if (index === activeNavItemIndex) { + additionalProps["active"] = true; + } + if (subItems) { + return ( + + + {subItems && ( + + {subItems.map((item, subIndex) => ( + + {item.label} + + ))} + + )} + + ); + } else { + return ( + + ); + } + })} + + + + ); +}; + +const MobileNavigation: FC> = ({ + closeMobileNav, +}) => { + return ( + + {navItems.map((navItem, index) => ( + + ))} + + ); +}; + +const MobileNavItem: FC< + NavItem & { + index: number; + closeMobileNav: NavigationMenuProps["closeMobileNav"]; + } +> = ({ closeMobileNav, ...props }) => { + const { isOpen, onToggle } = useDisclosure(); + + const onClickHandler = useCallback(() => { + if (props.subItems) { + // Toggle accordeon + onToggle(); + } else { + // Close menu on navigate + closeMobileNav(); + } + }, [closeMobileNav, onToggle, props.subItems]); + + return ( + + + + + {props.icon} + + {props.label} + + + {props.subItems && ( + + )} + + + + + {props.subItems && + props.subItems.map((subItem, subIndex) => ( + + {subItem.label} + + ))} + + + + + ); +}; + +export { NavigationMenu }; diff --git a/frontend/src/components/Navigation/index.ts b/frontend/src/components/Navigation/index.ts new file mode 100644 index 000000000..302ad0e69 --- /dev/null +++ b/frontend/src/components/Navigation/index.ts @@ -0,0 +1,3 @@ +export * from "./Navigation"; +export * from "./NavigationHeader"; +export * from "./NavigationMenu"; diff --git a/frontend/src/components/Permissions/AdminPermissionSelector.tsx b/frontend/src/components/Permissions/AdminPermissionSelector.tsx new file mode 100644 index 000000000..4f60e7dff --- /dev/null +++ b/frontend/src/components/Permissions/AdminPermissionSelector.tsx @@ -0,0 +1,37 @@ +import { MouseEventHandler } from "react"; + +import { Heading, Stack, Text, useColorModeValue } from "@chakra-ui/react"; + +import { intl } from "src/locale"; + +interface AdminPermissionSelectorProps { + selected?: boolean; + onClick: MouseEventHandler; +} + +function AdminPermissionSelector({ + selected, + onClick, +}: AdminPermissionSelectorProps) { + return ( + + + {intl.formatMessage({ id: "full-access" })} + + + {intl.formatMessage({ id: "full-access.description" })} + + + ); +} + +export { AdminPermissionSelector }; diff --git a/frontend/src/components/Permissions/PermissionSelector.tsx b/frontend/src/components/Permissions/PermissionSelector.tsx new file mode 100644 index 000000000..25ea95279 --- /dev/null +++ b/frontend/src/components/Permissions/PermissionSelector.tsx @@ -0,0 +1,286 @@ +import { ChangeEvent, MouseEventHandler } from "react"; + +import { + Flex, + Heading, + Select, + Stack, + Text, + useColorModeValue, +} from "@chakra-ui/react"; + +import { intl } from "src/locale"; + +interface PermissionSelectorProps { + capabilities: string[]; + selected?: boolean; + onClick: MouseEventHandler; + onChange: (i: string[]) => any; +} + +function PermissionSelector({ + capabilities, + selected, + onClick, + onChange, +}: PermissionSelectorProps) { + const textColor = useColorModeValue("gray.700", "gray.400"); + + const onSelectChange = ({ target }: ChangeEvent) => { + // remove all items starting with target.name + const i: string[] = []; + const re = new RegExp(`^${target.name}\\.`, "g"); + capabilities.forEach((capability) => { + if (!capability.match(re)) { + i.push(capability); + } + }); + + // add a new item, if value is something, and doesn't already exist + if (target.value) { + const c = `${target.name}.${target.value}`; + if (i.indexOf(c) === -1) { + i.push(c); + } + } + + onChange(i); + }; + + const getDefaultValue = (c: string): string => { + if (capabilities.indexOf(`${c}.manage`) !== -1) { + return "manage"; + } + if (capabilities.indexOf(`${c}.view`) !== -1) { + return "view"; + } + return ""; + }; + + return ( + + + {intl.formatMessage({ id: "restricted-access" })} + + {selected ? ( + + + + {intl.formatMessage({ id: "access-lists.title" })} + + + + + + + + {intl.formatMessage({ id: "audit-log.title" })} + + + + + + + + {intl.formatMessage({ id: "certificates.title" })} + + + + + + + + {intl.formatMessage({ id: "certificate-authorities.title" })} + + + + + + + + {intl.formatMessage({ id: "dns-providers.title" })} + + + + + + + {intl.formatMessage({ id: "hosts.title" })} + + + + + + + {intl.formatMessage({ id: "nginx-templates.title" })} + + + + + + + {intl.formatMessage({ id: "settings.title" })} + + + + + + {intl.formatMessage({ id: "users.title" })} + + + + + + ) : ( + + {intl.formatMessage({ id: "restricted-access.description" })} + + )} + + ); +} + +export { PermissionSelector }; diff --git a/frontend/src/components/Permissions/index.ts b/frontend/src/components/Permissions/index.ts new file mode 100644 index 000000000..e0cf81207 --- /dev/null +++ b/frontend/src/components/Permissions/index.ts @@ -0,0 +1,2 @@ +export * from "./AdminPermissionSelector"; +export * from "./PermissionSelector"; diff --git a/frontend/src/components/PrettyButton.tsx b/frontend/src/components/PrettyButton.tsx new file mode 100644 index 000000000..b717d0f7f --- /dev/null +++ b/frontend/src/components/PrettyButton.tsx @@ -0,0 +1,20 @@ +import { Button, ButtonProps } from "@chakra-ui/react"; + +function PrettyButton(props: ButtonProps) { + return ( + + ); +} + +export { PrettyButton }; diff --git a/frontend/src/components/PrettyMenuButton.tsx b/frontend/src/components/PrettyMenuButton.tsx new file mode 100644 index 000000000..554a078cf --- /dev/null +++ b/frontend/src/components/PrettyMenuButton.tsx @@ -0,0 +1,23 @@ +import { Button, MenuButton, MenuButtonProps } from "@chakra-ui/react"; +import { FiChevronDown } from "react-icons/fi"; + +function PrettyMenuButton(props: MenuButtonProps) { + return ( + } + _hover={{ + bgGradient: "linear(to-r, red.400,pink.400)", + boxShadow: "xl", + }} + {...props}> + {props.children} + + ); +} + +export { PrettyMenuButton }; diff --git a/frontend/src/components/SiteWrapper.tsx b/frontend/src/components/SiteWrapper.tsx new file mode 100644 index 000000000..4062da248 --- /dev/null +++ b/frontend/src/components/SiteWrapper.tsx @@ -0,0 +1,78 @@ +import { ReactNode, useEffect } from "react"; + +import { Box, Container, useToast } from "@chakra-ui/react"; +import { useQueryClient } from "@tanstack/react-query"; + +import { getSSEToken, SSEMessage } from "src/api/npm"; +import { Footer, Navigation } from "src/components"; +import { intl } from "src/locale"; +import AuthStore from "src/modules/AuthStore"; + +interface Props { + children?: ReactNode; +} +function SiteWrapper({ children }: Props) { + const queryClient = useQueryClient(); + const toast = useToast(); + + // TODO: fix bug where this will fail if the browser is kept open longer + // than the expiry of the sse token + useEffect(() => { + async function fetchData() { + const response = await getSSEToken(); + const eventSource = new EventSource( + `/api/sse/changes?jwt=${response.token}`, + ); + eventSource.onmessage = (e: any) => { + const j: SSEMessage = JSON.parse(e.data); + if (j) { + console.log("SSE Message:", j); + if (j.affects) { + queryClient.invalidateQueries({ queryKey: [j.affects] }); + } + if (j.type) { + toast({ + description: intl.formatMessage({ id: j.lang }), + status: j.type || "info", + position: "top", + duration: 3000, + isClosable: true, + }); + } + } + }; + eventSource.onerror = (e) => { + console.error("SSE EventSource failed:", e); + }; + return () => { + eventSource.close(); + }; + } + if (AuthStore.token) { + fetchData(); + } + }, [queryClient, toast]); + + return ( + + + + + + + {children} + + + +
+ + + ); +} + +export { SiteWrapper }; diff --git a/frontend/src/components/SpinnerPage.tsx b/frontend/src/components/SpinnerPage.tsx new file mode 100644 index 000000000..1b77389e6 --- /dev/null +++ b/frontend/src/components/SpinnerPage.tsx @@ -0,0 +1,9 @@ +import { Flex, Spinner } from "@chakra-ui/react"; + +export function SpinnerPage() { + return ( + + + + ); +} diff --git a/frontend/src/components/Table/Formatters.tsx b/frontend/src/components/Table/Formatters.tsx new file mode 100644 index 000000000..b8e6baaf5 --- /dev/null +++ b/frontend/src/components/Table/Formatters.tsx @@ -0,0 +1,337 @@ +import { + Avatar, + Badge, + Text, + Tooltip, + Popover, + PopoverTrigger, + PopoverContent, + PopoverArrow, + PopoverBody, +} from "@chakra-ui/react"; + +import { Monospace, RowAction, RowActionsMenu } from "src/components"; +import { intl } from "src/locale"; +import getNiceDNSProvider from "src/modules/Acmesh"; + +const errorColor = "red.400"; + +function ActionsFormatter(rowActions: RowAction[]) { + const formatCell = (instance: any) => { + return ; + }; + + return formatCell; +} + +function BooleanFormatter() { + const formatCell = ({ value }: any) => { + return ( + + {value ? "true" : "false"} + + ); + }; + + return formatCell; +} + +function CapabilitiesFormatter() { + const formatCell = ({ row, value }: any) => { + const style = {} as any; + if (row?.original?.isDisabled) { + style.textDecoration = "line-through"; + } + + if (row?.original?.isSystem) { + return ( + + {intl.formatMessage({ id: "capability.system" })} + + ); + } + + if (value?.indexOf("full-admin") !== -1) { + return ( + + {intl.formatMessage({ id: "capability.full-admin" })} + + ); + } + + if (value?.length) { + const strs: string[] = []; + value.map((c: string) => { + strs.push(intl.formatMessage({ id: `capability.${c}` })); + return null; + }); + + return ( + + + {intl.formatMessage( + { id: "capability-count" }, + { count: value.length }, + )} + + + ); + } + + return null; + }; + + return formatCell; +} + +function CertificateStatusFormatter() { + const formatCell = ({ value, row }: any) => { + let color = "cyan.500"; + switch (value) { + case "failed": + color = errorColor; + break; + case "provided": + color = "green.400"; + break; + case "requesting": + color = "yellow.400"; + break; + } + // special case for failed to show an error popover + if (value === "failed" && row?.original?.errorMessage) { + return ( + + + + {intl.formatMessage({ id: `status.${value}` })} + + + + + +
+								{row?.original?.errorMessage}
+							
+
+
+
+ ); + } + return ( + + {intl.formatMessage({ id: `status.${value}` })} + + ); + }; + + return formatCell; +} + +function CertificateTypeFormatter() { + const formatCell = ({ value }: any) => { + let color = "cyan.500"; + if (value === "dns") { + color = "green.400"; + } + return ( + {intl.formatMessage({ id: `type.${value}` })} + ); + }; + + return formatCell; +} + +function DisabledFormatter() { + const formatCell = ({ value, row }: any) => { + if (row?.original?.isDisabled) { + return ( + + + {value} + + + ); + } + return value; + }; + + return formatCell; +} + +function DNSProviderFormatter() { + const formatCell = ({ value }: any) => { + return getNiceDNSProvider(value); + }; + + return formatCell; +} + +function DomainsFormatter() { + const formatCell = ({ value }: any) => { + if (value?.length > 0) { + return ( + <> + {value.map((dom: string, idx: number) => { + return ( + + {dom} + + ); + })} + + ); + } + return No domains!; + }; + + return formatCell; +} + +function GravatarFormatter() { + const formatCell = ({ value }: any) => { + return ; + }; + + return formatCell; +} + +function HostStatusFormatter() { + const formatCell = ({ row }: any) => { + if (row.original.isDisabled) { + return ( + + {intl.formatMessage({ id: "disabled" })} + + ); + } + + if (row.original.certificateId) { + if (row.original.certificate.status === "provided") { + return ( + + {row.original.sslForced + ? intl.formatMessage({ id: "https-only" }) + : intl.formatMessage({ id: "http-https" })} + + ); + } + + if (row.original.certificate.status === "error") { + return ( + + + {intl.formatMessage({ id: "error" })} + + + ); + } + + return ( + + {intl.formatMessage({ + id: `certificate.${row.original.certificate.status}`, + })} + + ); + } + + return ( + + {intl.formatMessage({ id: "http-only" })} + + ); + }; + + return formatCell; +} + +function MonospaceFormatter() { + const formatCell = ({ value }: any) => { + return {value}; + }; + + return formatCell; +} + +function UpstreamStatusFormatter() { + const formatCell = ({ value, row }: any) => { + if (value === "ready") { + return ( + + {intl.formatMessage({ id: "status.ready" })} + + ); + } + if (value === "ok") { + return ( + + {intl.formatMessage({ id: "status.ok" })} + + ); + } + if (value === "error") { + return ( + + + + {intl.formatMessage({ id: "error" })} + + + + + +
+								{row?.original?.errorMessage}
+							
+
+
+
+ ); + } + }; + + return formatCell; +} + +function HostTypeFormatter() { + const formatCell = ({ value }: any) => { + return intl.formatMessage({ id: `host-type.${value}` }); + }; + + return formatCell; +} + +function IDFormatter() { + const formatCell = ({ value }: any) => { + return {value}; + }; + + return formatCell; +} + +function SecondsFormatter() { + const formatCell = ({ value }: any) => { + return intl.formatMessage({ id: "seconds" }, { seconds: value }); + }; + + return formatCell; +} + +export { + ActionsFormatter, + BooleanFormatter, + CapabilitiesFormatter, + CertificateStatusFormatter, + CertificateTypeFormatter, + DisabledFormatter, + DNSProviderFormatter, + DomainsFormatter, + GravatarFormatter, + HostStatusFormatter, + HostTypeFormatter, + IDFormatter, + MonospaceFormatter, + SecondsFormatter, + UpstreamStatusFormatter, +}; diff --git a/frontend/src/components/Table/RowActionsMenu.tsx b/frontend/src/components/Table/RowActionsMenu.tsx new file mode 100644 index 000000000..6e0936040 --- /dev/null +++ b/frontend/src/components/Table/RowActionsMenu.tsx @@ -0,0 +1,70 @@ +import { ReactNode } from "react"; + +import { + Menu, + MenuButton, + MenuList, + MenuItem, + IconButton, +} from "@chakra-ui/react"; +import { FiMoreVertical } from "react-icons/fi"; + +// A row action is a single menu item for the actions column +export interface RowAction { + title: string; + onClick: (e: any, data: any) => any; + show?: (data: any) => any; + disabled?: (data: any) => any; + icon?: any; +} + +interface RowActionsProps { + /** + * Row Data + */ + data: any; + /** + * Actions + */ + actions: RowAction[]; +} +function RowActionsMenu({ data, actions }: RowActionsProps) { + const elms: ReactNode[] = []; + actions.map((action) => { + if (!action.show || action.show(data)) { + const disabled = action.disabled && action.disabled(data); + elms.push( + { + action.onClick(e, data); + }}> + {action.title} + , + ); + } + return null; + }); + + if (!elms.length) { + return null; + } + + return ( +
+ + } + variant="outline" + /> + {elms} + +
+ ); +} + +export { RowActionsMenu }; diff --git a/frontend/src/components/Table/TableHelpers.ts b/frontend/src/components/Table/TableHelpers.ts new file mode 100644 index 000000000..0df329c80 --- /dev/null +++ b/frontend/src/components/Table/TableHelpers.ts @@ -0,0 +1,64 @@ +export interface TablePagination { + limit: number; + offset: number; + total: number; +} + +export interface TableSortBy { + id: string; + desc: boolean; +} + +export interface TableFilter { + id: string; + value: any; +} + +const tableEvents = { + FILTERS_CHANGED: "FILTERS_CHANGED", + PAGE_CHANGED: "PAGE_CHANGED", + PAGE_SIZE_CHANGED: "PAGE_SIZE_CHANGED", + TOTAL_COUNT_CHANGED: "TOTAL_COUNT_CHANGED", + SORT_CHANGED: "SORT_CHANGED", +}; + +const tableEventReducer = (state: any, { type, payload }: any) => { + let offset = state.offset; + switch (type) { + case tableEvents.PAGE_CHANGED: + return { + ...state, + offset: payload * state.limit, + }; + case tableEvents.PAGE_SIZE_CHANGED: + return { + ...state, + limit: payload, + }; + case tableEvents.TOTAL_COUNT_CHANGED: + return { + ...state, + total: payload, + }; + case tableEvents.SORT_CHANGED: + return { + ...state, + sortBy: payload, + }; + case tableEvents.FILTERS_CHANGED: + if (state.filters !== payload) { + // this actually was a legit change + // sets to page 1 when filter is modified + offset = 0; + } + return { + ...state, + filters: payload, + offset, + }; + default: + throw new Error(`Unhandled action type: ${type}`); + } +}; + +export { tableEvents, tableEventReducer }; diff --git a/frontend/src/components/Table/TableLayout.tsx b/frontend/src/components/Table/TableLayout.tsx new file mode 100644 index 000000000..cf8df7bd4 --- /dev/null +++ b/frontend/src/components/Table/TableLayout.tsx @@ -0,0 +1,251 @@ +import { ReactNode } from "react"; + +import { + ButtonGroup, + Center, + Flex, + HStack, + IconButton, + Link, + Select, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + VStack, +} from "@chakra-ui/react"; +import { + FiChevronsLeft, + FiChevronLeft, + FiChevronsRight, + FiChevronRight, + FiChevronDown, + FiChevronUp, + FiX, +} from "react-icons/fi"; + +import { TablePagination } from "src/components"; +import { intl } from "src/locale"; + +export interface TableLayoutProps { + pagination: TablePagination; + getTableProps: any; + getTableBodyProps: any; + headerGroups: any; + rows: any; + prepareRow: any; + gotoPage: any; + canPreviousPage: any; + previousPage: any; + canNextPage: any; + nextPage: any; + pageCount: any; + pageOptions: any; + visibleColumns: any; + setAllFilters: any; + state: any; +} +function TableLayout({ + pagination, + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + gotoPage, + canPreviousPage, + previousPage, + canNextPage, + nextPage, + pageCount, + pageOptions, + visibleColumns, + setAllFilters, + state, +}: TableLayoutProps) { + const currentPage = state.pageIndex + 1; + const getPageList = () => { + const list = []; + for (let x = 0; x < pageOptions.length; x++) { + list.push( + , + ); + } + return list; + }; + + const renderEmpty = (): ReactNode => { + return ( +
+ + + ); + }; + + return ( + <> +
-
- -
-
-
- <% 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') %> -
 UserEvent 
-
- -
-
-
- <%- 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}) %> - - -
 <%- i18n('str', 'name') %><%- i18n('access-lists', 'authorization') %><%- i18n('access-lists', 'access') %><%- i18n('access-lists', 'satisfy') %><%- i18n('proxy-hosts', 'title') %> 
-
- -
-
-
- <% - 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') %> - - -
 <%- i18n('str', 'name') %><%- i18n('all-hosts', 'cert-provider') %><%- i18n('str', 'expires') %> 
-
- -
-
-
- <% 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') %> - <% } %> - - -
 <%- i18n('str', 'source') %><%- i18n('str', 'ssl') %><%- i18n('str', 'status') %> 
-
- -
-
-
- <% 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') %> - <% } %> - - -
 <%- i18n('str', 'source') %><%- i18n('str', 'destination') %><%- i18n('str', 'ssl') %><%- i18n('str', 'access') %><%- i18n('str', 'status') %> 
-
- -
-
-
- <% 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') %> - <% } %> - - -
 <%- i18n('str', 'source') %><%- i18n('redirection-hosts', 'forward-http-status-code') %><%- i18n('redirection-hosts', 'forward-scheme') %><%- i18n('str', 'destination') %><%- i18n('str', 'ssl') %><%- i18n('str', 'status') %> 
-
- -
-
-
- <%- incoming_port %> -
-
- <%- i18n('str', 'created-on', {date: formatDbDate(created_on, 'Do MMMM YYYY')}) %> -
-
-
<%- forwarding_host %>:<%- 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') %> - <% } %> - - -
 <%- i18n('streams', 'incoming-port') %><%- i18n('str', 'destination') %><%- i18n('streams', 'protocol') %><%- i18n('str', 'status') %> 
-
<%- name %>
-
- <%- description %> -
-
-
- <% if (id === 'default-site') { %> - <%- i18n('settings', 'default-site-' + value) %> - <% } %> -
-
- -
<%- i18n('str', 'name') %><%- i18n('str', 'value') %> 
-
- -
-
-
<%- 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(', ') %> -
-
- -
 <%- i18n('str', 'name') %><%- i18n('str', 'email') %><%- i18n('str', 'roles') %> 
+
+ {state?.filters?.length + ? intl.formatMessage( + { id: "tables.no-items-with-filters" }, + { count: state.filters.length }, + ) + : intl.formatMessage({ id: "tables.no-items" })} +
+
+ + {headerGroups.map((headerGroup: any, idx: any) => ( + + {headerGroup.headers.map((column: any, idx2: any) => ( + + ))} + + ))} + + + {rows.length + ? rows.map((row: any, idx: any) => { + prepareRow(row); + return ( + + {row.cells.map((cell: any, idx2: any) => ( + + ))} + + ); + }) + : renderEmpty()} + +
+ + + + {column.render("Header")} + + {column.sortable && column.isSorted ? ( + column.isSortedDesc ? ( + + ) : ( + + ) + ) : null} + {column.Filter ? column.render("Filter") : null} + + +
+ {cell.render("Cell")} +
+ + + + {rows.length + ? intl.formatMessage( + { id: "tables.pagination-counts" }, + { + start: pagination.offset + 1, + end: Math.min( + pagination.total, + pagination.offset + pagination.limit, + ), + total: pagination.total, + }, + ) + : null} + + {state?.filters?.length ? ( + setAllFilters([])}> + + + + {intl.formatMessage( + { id: "tables.clear-all-filters" }, + { count: state.filters.length }, + )} + + + + ) : null} + + + + + ); +} + +export { TableLayout }; diff --git a/frontend/src/components/Table/TextFilter.tsx b/frontend/src/components/Table/TextFilter.tsx new file mode 100644 index 000000000..d1eeef7f1 --- /dev/null +++ b/frontend/src/components/Table/TextFilter.tsx @@ -0,0 +1,143 @@ +import { + Popover, + PopoverTrigger, + PopoverContent, + PopoverArrow, + IconButton, + FormControl, + FormErrorMessage, + Input, + Stack, + ButtonGroup, + Button, + useDisclosure, + Select, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; +import FocusLock from "react-focus-lock"; +import { FiFilter } from "react-icons/fi"; + +import { PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +function TextFilter({ column: { filterValue, setFilter } }: any) { + const { onOpen, onClose, isOpen } = useDisclosure(); + + const onSubmit = (values: any, { setSubmitting }: any) => { + setFilter(values); + setSubmitting(false); + onClose(); + }; + + const clearFilter = () => { + setFilter(undefined); + onClose(); + }; + + const isFiltered = (): boolean => { + return !(typeof filterValue === "undefined" || filterValue === ""); + }; + + return ( + + + } + aria-label="Filter" + /> + + + + + + {({ isSubmitting }) => ( +
+ + + {({ field, form }: any) => ( + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {form.errors.value} + + )} + + + + + {intl.formatMessage({ + id: "filter.apply", + })} + + + +
+ )} +
+
+
+
+ ); +} + +export { TextFilter }; diff --git a/frontend/src/components/Table/index.ts b/frontend/src/components/Table/index.ts new file mode 100644 index 000000000..16d6d0a60 --- /dev/null +++ b/frontend/src/components/Table/index.ts @@ -0,0 +1,5 @@ +export * from "./Formatters"; +export * from "./RowActionsMenu"; +export * from "./TableHelpers"; +export * from "./TableLayout"; +export * from "./TextFilter"; diff --git a/frontend/src/components/Table/react-table-config.d.ts b/frontend/src/components/Table/react-table-config.d.ts new file mode 100644 index 000000000..59f716890 --- /dev/null +++ b/frontend/src/components/Table/react-table-config.d.ts @@ -0,0 +1,129 @@ +// See: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-table#configuration-using-declaration-merging +import { + UseColumnOrderInstanceProps, + UseColumnOrderState, + UseExpandedHooks, + UseExpandedInstanceProps, + UseExpandedOptions, + UseExpandedRowProps, + UseExpandedState, + UseFiltersColumnOptions, + UseFiltersColumnProps, + UseFiltersInstanceProps, + UseFiltersOptions, + UseFiltersState, + UseGlobalFiltersColumnOptions, + UseGlobalFiltersInstanceProps, + UseGlobalFiltersOptions, + UseGlobalFiltersState, + UseGroupByCellProps, + UseGroupByColumnOptions, + UseGroupByColumnProps, + UseGroupByHooks, + UseGroupByInstanceProps, + UseGroupByOptions, + UseGroupByRowProps, + UseGroupByState, + UsePaginationInstanceProps, + UsePaginationOptions, + UsePaginationState, + UseResizeColumnsColumnOptions, + UseResizeColumnsColumnProps, + UseResizeColumnsOptions, + UseResizeColumnsState, + UseRowSelectHooks, + UseRowSelectInstanceProps, + UseRowSelectOptions, + UseRowSelectRowProps, + UseRowSelectState, + UseRowStateCellProps, + UseRowStateInstanceProps, + UseRowStateOptions, + UseRowStateRowProps, + UseRowStateState, + UseSortByColumnOptions, + UseSortByColumnProps, + UseSortByHooks, + UseSortByInstanceProps, + UseSortByOptions, + UseSortByState, +} from "react-table"; + +declare module "react-table" { + // take this file as-is, or comment out the sections that don't apply to your plugin configuration + + export interface TableOptions> + extends UseExpandedOptions, + UseFiltersOptions, + UseGlobalFiltersOptions, + UseGroupByOptions, + UsePaginationOptions, + UseResizeColumnsOptions, + UseRowSelectOptions, + UseRowStateOptions, + UseSortByOptions, + // note that having Record here allows you to add anything to the options, this matches the spirit of the + // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your + // feature set, this is a safe default. + Record {} + + export interface Hooks< + D extends Record = Record, + > extends UseExpandedHooks, + UseGroupByHooks, + UseRowSelectHooks, + UseSortByHooks {} + + export interface TableInstance< + D extends Record = Record, + > extends UseColumnOrderInstanceProps, + UseExpandedInstanceProps, + UseFiltersInstanceProps, + UseGlobalFiltersInstanceProps, + UseGroupByInstanceProps, + UsePaginationInstanceProps, + UseRowSelectInstanceProps, + UseRowStateInstanceProps, + UseSortByInstanceProps {} + + export interface TableState< + D extends Record = Record, + > extends UseColumnOrderState, + UseExpandedState, + UseFiltersState, + UseGlobalFiltersState, + UseGroupByState, + UsePaginationState, + UseResizeColumnsState, + UseRowSelectState, + UseRowStateState, + UseSortByState {} + + export interface ColumnInterface< + D extends Record = Record, + > extends UseFiltersColumnOptions, + UseGlobalFiltersColumnOptions, + UseGroupByColumnOptions, + UseResizeColumnsColumnOptions, + UseSortByColumnOptions {} + + export interface ColumnInstance< + D extends Record = Record, + > extends UseFiltersColumnProps, + UseGroupByColumnProps, + UseResizeColumnsColumnProps, + UseSortByColumnProps {} + + export interface Cell< + D extends Record = Record, + // V = any, + > extends UseGroupByCellProps, + UseRowStateCellProps {} + + export interface Row< + D extends Record = Record, + > extends UseExpandedRowProps, + UseGroupByRowProps, + UseRowSelectRowProps, + UseRowStateRowProps {} +} diff --git a/frontend/src/components/ThemeSwitcher.tsx b/frontend/src/components/ThemeSwitcher.tsx new file mode 100644 index 000000000..af155100c --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.tsx @@ -0,0 +1,35 @@ +import { + Icon, + IconButton, + IconButtonProps, + useColorMode, +} from "@chakra-ui/react"; +import { FiSun, FiMoon } from "react-icons/fi"; + +import { intl } from "src/locale"; + +interface ThemeSwitcherProps { + background?: "normal" | "transparent"; +} +function ThemeSwitcher({ background }: ThemeSwitcherProps) { + const { colorMode, toggleColorMode } = useColorMode(); + const additionalProps: Partial = {}; + if (background === "transparent") { + additionalProps["backgroundColor"] = "transparent"; + } + + return ( + } + /> + ); +} + +export { ThemeSwitcher }; diff --git a/frontend/src/components/Unhealthy.tsx b/frontend/src/components/Unhealthy.tsx new file mode 100644 index 000000000..ee028d5cf --- /dev/null +++ b/frontend/src/components/Unhealthy.tsx @@ -0,0 +1,39 @@ +import { Box, Flex, Heading, Text, Stack } from "@chakra-ui/react"; +import { FaTimes } from "react-icons/fa"; + +import { LocalePicker, ThemeSwitcher } from "src/components"; +import { intl } from "src/locale"; + +function Unhealthy() { + return ( + <> + + + + + + + + + + + + {intl.formatMessage({ id: "unhealthy.title" })} + + + {intl.formatMessage({ id: "unhealthy.body" })} + + + + ); +} + +export { Unhealthy }; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 000000000..3f88be3ed --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,17 @@ +export * from "./EmptyList"; +export * from "./Flag"; +export * from "./Footer"; +export * from "./HelpDrawer"; +export * from "./Loader"; +export * from "./Loading"; +export * from "./LocalePicker"; +export * from "./Monospace"; +export * from "./Navigation"; +export * from "./Permissions"; +export * from "./PrettyButton"; +export * from "./PrettyMenuButton"; +export * from "./SiteWrapper"; +export * from "./SpinnerPage"; +export * from "./Table"; +export * from "./ThemeSwitcher"; +export * from "./Unhealthy"; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 000000000..c3a196e8c --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,80 @@ +import { createContext, ReactNode, useContext, useState } from "react"; + +import { useQueryClient } from "@tanstack/react-query"; +import { useIntervalWhen } from "rooks"; + +import { getToken, refreshToken, TokenResponse } from "src/api/npm"; +import AuthStore from "src/modules/AuthStore"; + +// Context +export interface AuthContextType { + authenticated: boolean; + handleTokenUpdate: (response: TokenResponse) => void; + login: (type: string, username: string, password: string) => Promise; + logout: () => void; + token?: string; +} + +const initalValue = null; +const AuthContext = createContext(initalValue); + +// Provider +interface Props { + children?: ReactNode; + tokenRefreshInterval?: number; +} +function AuthProvider({ + children, + tokenRefreshInterval = 5 * 60 * 1000, +}: Props) { + const queryClient = useQueryClient(); + const [authenticated, setAuthenticated] = useState( + AuthStore.hasActiveToken(), + ); + + const handleTokenUpdate = (response: TokenResponse) => { + AuthStore.set(response); + setAuthenticated(true); + }; + + const login = async (type: string, identity: string, secret: string) => { + const response = await getToken({ payload: { type, identity, secret } }); + handleTokenUpdate(response); + }; + + const logout = () => { + AuthStore.clear(); + setAuthenticated(false); + queryClient.clear(); + }; + + const refresh = async () => { + const response = await refreshToken(); + handleTokenUpdate(response); + }; + + useIntervalWhen( + () => { + if (authenticated) { + refresh(); + } + }, + tokenRefreshInterval, + true, + ); + + const value = { authenticated, login, logout, handleTokenUpdate }; + + return {children}; +} + +function useAuthState() { + const context = useContext(AuthContext); + if (!context) { + throw new Error(`useAuthState must be used within a AuthProvider`); + } + return context; +} + +export { AuthProvider, useAuthState }; +export default AuthContext; diff --git a/frontend/src/context/LocaleContext.tsx b/frontend/src/context/LocaleContext.tsx new file mode 100644 index 000000000..4056e52fd --- /dev/null +++ b/frontend/src/context/LocaleContext.tsx @@ -0,0 +1,41 @@ +import { createContext, ReactNode, useContext, useState } from "react"; + +import { getLocale } from "src/locale"; + +// Context +export interface LocaleContextType { + setLocale: (locale: string) => void; + locale?: string; +} + +const initalValue = null; +const LocaleContext = createContext(initalValue); + +// Provider +interface Props { + children?: ReactNode; +} +function LocaleProvider({ children }: Props) { + const [locale, setLocaleValue] = useState(getLocale()); + + const setLocale = async (locale: string) => { + setLocaleValue(locale); + }; + + const value = { locale, setLocale }; + + return ( + {children} + ); +} + +function useLocaleState() { + const context = useContext(LocaleContext); + if (!context) { + throw new Error(`useLocaleState must be used within a LocaleProvider`); + } + return context; +} + +export { LocaleProvider, useLocaleState }; +export default LocaleContext; diff --git a/frontend/src/context/index.ts b/frontend/src/context/index.ts new file mode 100644 index 000000000..224db0581 --- /dev/null +++ b/frontend/src/context/index.ts @@ -0,0 +1,2 @@ +export * from "./AuthContext"; +export * from "./LocaleContext"; diff --git a/frontend/src/declarations.d.ts b/frontend/src/declarations.d.ts new file mode 100644 index 000000000..18f423bf3 --- /dev/null +++ b/frontend/src/declarations.d.ts @@ -0,0 +1 @@ +declare module "*.md"; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 000000000..ca1d1842e --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1,16 @@ +export * from "./useAccessLists"; +export * from "./useCertificate"; +export * from "./useCertificateAuthorities"; +export * from "./useCertificateAuthority"; +export * from "./useCertificates"; +export * from "./useDNSProvider"; +export * from "./useDNSProviders"; +export * from "./useDNSProvidersAcmesh"; +export * from "./useHealth"; +export * from "./useHosts"; +export * from "./useNginxTemplates"; +export * from "./useSettings"; +export * from "./useUpstreamNginxConfig"; +export * from "./useUpstreams"; +export * from "./useUser"; +export * from "./useUsers"; diff --git a/frontend/src/hooks/useAccessLists.ts b/frontend/src/hooks/useAccessLists.ts new file mode 100644 index 000000000..4dff5d029 --- /dev/null +++ b/frontend/src/hooks/useAccessLists.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getAccessLists, + AccessListsResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchAccessLists = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getAccessLists( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useAccessLists = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["access-lists", { offset, limit, sortBy, filters }], + queryFn: () => fetchAccessLists(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { useAccessLists }; diff --git a/frontend/src/hooks/useCertificate.ts b/frontend/src/hooks/useCertificate.ts new file mode 100644 index 000000000..87691f9f0 --- /dev/null +++ b/frontend/src/hooks/useCertificate.ts @@ -0,0 +1,53 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { + createCertificate, + getCertificate, + setCertificate, + Certificate, +} from "src/api/npm"; + +const fetchCertificate = (id: any) => { + return getCertificate(id); +}; + +const useCertificate = (id: number, options = {}) => { + return useQuery({ + queryKey: ["certificate", id], + queryFn: () => fetchCertificate(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetCertificate = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: Certificate) => { + return values.id + ? setCertificate(values.id, values) + : createCertificate(values); + }, + onMutate: (values: Certificate) => { + const previousObject = queryClient.getQueryData([ + "certificate", + values.id, + ]); + + queryClient.setQueryData(["certificate", values.id], (old: any) => ({ + ...old, + ...values, + })); + + return () => + queryClient.setQueryData(["certificate", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: Certificate) => { + queryClient.invalidateQueries({ queryKey: ["certificate", id] }); + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, + }); +}; + +export { useCertificate, useSetCertificate }; diff --git a/frontend/src/hooks/useCertificateAuthorities.ts b/frontend/src/hooks/useCertificateAuthorities.ts new file mode 100644 index 000000000..a4bcc9325 --- /dev/null +++ b/frontend/src/hooks/useCertificateAuthorities.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + CertificateAuthoritiesResponse, + getCertificateAuthorities, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchCertificateAuthorities = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getCertificateAuthorities( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useCertificateAuthorities = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["certificate-authorities", { offset, limit, sortBy, filters }], + queryFn: () => fetchCertificateAuthorities(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchCertificateAuthorities, useCertificateAuthorities }; diff --git a/frontend/src/hooks/useCertificateAuthority.ts b/frontend/src/hooks/useCertificateAuthority.ts new file mode 100644 index 000000000..15814e30e --- /dev/null +++ b/frontend/src/hooks/useCertificateAuthority.ts @@ -0,0 +1,63 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { + createCertificateAuthority, + getCertificateAuthority, + setCertificateAuthority, + CertificateAuthority, +} from "src/api/npm"; + +const fetchCertificateAuthority = (id: any) => { + return getCertificateAuthority(id); +}; + +const useCertificateAuthority = (id: number, options = {}) => { + return useQuery({ + queryKey: ["certificate-authority", id], + queryFn: () => fetchCertificateAuthority(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetCertificateAuthority = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: CertificateAuthority) => { + return values.id + ? setCertificateAuthority(values.id, values) + : createCertificateAuthority(values); + }, + onMutate: (values: CertificateAuthority) => { + const previousObject = queryClient.getQueryData([ + "certificate-authority", + values.id, + ]); + + queryClient.setQueryData( + ["certificate-authority", values.id], + (old: any) => ({ + ...old, + ...values, + }), + ); + + return () => + queryClient.setQueryData( + ["certificate-authority", values.id], + previousObject, + ); + }, + onError: (_: any, __: any, rollback: any) => rollback(), + onSuccess: async ({ id }: CertificateAuthority) => { + queryClient.invalidateQueries({ + queryKey: ["certificate-authority", id], + }); + queryClient.invalidateQueries({ + queryKey: ["certificate-authorities"], + }); + }, + }); +}; + +export { useCertificateAuthority, useSetCertificateAuthority }; diff --git a/frontend/src/hooks/useCertificates.ts b/frontend/src/hooks/useCertificates.ts new file mode 100644 index 000000000..26557e520 --- /dev/null +++ b/frontend/src/hooks/useCertificates.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getCertificates, + CertificatesResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchCertificates = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getCertificates( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useCertificates = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["certificates", { offset, limit, sortBy, filters }], + queryFn: () => fetchCertificates(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchCertificates, useCertificates }; diff --git a/frontend/src/hooks/useDNSProvider.ts b/frontend/src/hooks/useDNSProvider.ts new file mode 100644 index 000000000..17260ed4f --- /dev/null +++ b/frontend/src/hooks/useDNSProvider.ts @@ -0,0 +1,53 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { + createDNSProvider, + getDNSProvider, + setDNSProvider, + DNSProvider, +} from "src/api/npm"; + +const fetchDNSProvider = (id: any) => { + return getDNSProvider(id); +}; + +const useDNSProvider = (id: number, options = {}) => { + return useQuery({ + queryKey: ["dns-provider", id], + queryFn: () => fetchDNSProvider(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetDNSProvider = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: DNSProvider) => { + return values.id + ? setDNSProvider(values.id, values) + : createDNSProvider(values); + }, + onMutate: (values: DNSProvider) => { + const previousObject = queryClient.getQueryData([ + "dns-provider", + values.id, + ]); + + queryClient.setQueryData(["dns-provider", values.id], (old: any) => ({ + ...old, + ...values, + })); + + return () => + queryClient.setQueryData(["dns-provider", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: DNSProvider) => { + queryClient.invalidateQueries({ queryKey: ["dns-provider", id] }); + queryClient.invalidateQueries({ queryKey: ["dns-providers"] }); + }, + }); +}; + +export { useDNSProvider, useSetDNSProvider }; diff --git a/frontend/src/hooks/useDNSProviders.ts b/frontend/src/hooks/useDNSProviders.ts new file mode 100644 index 000000000..0a9fbbb89 --- /dev/null +++ b/frontend/src/hooks/useDNSProviders.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getDNSProviders, + DNSProvidersResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchDNSProviders = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getDNSProviders( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useDNSProviders = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["dns-providers", { offset, limit, sortBy, filters }], + queryFn: () => fetchDNSProviders(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { useDNSProviders }; diff --git a/frontend/src/hooks/useDNSProvidersAcmesh.ts b/frontend/src/hooks/useDNSProvidersAcmesh.ts new file mode 100644 index 000000000..1b92349e3 --- /dev/null +++ b/frontend/src/hooks/useDNSProvidersAcmesh.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; + +import { DNSProvidersAcmesh, getDNSProvidersAcmesh } from "src/api/npm"; + +const useDNSProvidersAcmesh = (options = {}) => { + return useQuery({ + queryKey: ["dns-providers-acmesh"], + queryFn: () => getDNSProvidersAcmesh(), + staleTime: 60 * 60 * 1000, // 1 hour + ...options, + }); +}; + +export { useDNSProvidersAcmesh }; diff --git a/frontend/src/hooks/useHealth.ts b/frontend/src/hooks/useHealth.ts new file mode 100644 index 000000000..91a177123 --- /dev/null +++ b/frontend/src/hooks/useHealth.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; + +import { getHealth, HealthResponse } from "src/api/npm"; + +const fetchHealth = () => getHealth(); + +const useHealth = (options = {}) => { + return useQuery({ + queryKey: ["health"], + queryFn: fetchHealth, + refetchOnWindowFocus: false, + retry: 5, + refetchInterval: 15 * 1000, // 15 seconds + staleTime: 14 * 1000, // 14 seconds + ...options, + }); +}; + +export { fetchHealth, useHealth }; diff --git a/frontend/src/hooks/useHosts.ts b/frontend/src/hooks/useHosts.ts new file mode 100644 index 000000000..fda9f1606 --- /dev/null +++ b/frontend/src/hooks/useHosts.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getHosts, + HostsResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchHosts = (offset = 0, limit = 10, sortBy?: any, filters?: any) => { + return getHosts( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useHosts = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["hosts", { offset, limit, sortBy, filters }], + queryFn: () => fetchHosts(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchHosts, useHosts }; diff --git a/frontend/src/hooks/useNginxTemplates.ts b/frontend/src/hooks/useNginxTemplates.ts new file mode 100644 index 000000000..4faf09506 --- /dev/null +++ b/frontend/src/hooks/useNginxTemplates.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getNginxTemplates, + NginxTemplatesResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchNginxTemplates = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getNginxTemplates( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useNginxTemplates = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["nginx-templates", { offset, limit, sortBy, filters }], + queryFn: () => fetchNginxTemplates(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchNginxTemplates, useNginxTemplates }; diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts new file mode 100644 index 000000000..d452e8c8b --- /dev/null +++ b/frontend/src/hooks/useSettings.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getSettings, + SettingsResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchSettings = (offset = 0, limit = 10, sortBy?: any, filters?: any) => { + return getSettings( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useSettings = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["settings", { offset, limit, sortBy, filters }], + queryFn: () => fetchSettings(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchSettings, useSettings }; diff --git a/frontend/src/hooks/useUpstreamNginxConfig.ts b/frontend/src/hooks/useUpstreamNginxConfig.ts new file mode 100644 index 000000000..675a4c2c0 --- /dev/null +++ b/frontend/src/hooks/useUpstreamNginxConfig.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@tanstack/react-query"; + +import { getUpstreamNginxConfig } from "src/api/npm"; + +const fetchUpstreamNginxConfig = (id: any) => { + return getUpstreamNginxConfig(id); +}; + +const useUpstreamNginxConfig = (id: number, options = {}) => { + return useQuery({ + queryKey: ["upstream-nginx-config", id], + queryFn: () => fetchUpstreamNginxConfig(id), + staleTime: 30 * 1000, // 30 seconds + ...options, + }); +}; + +export { useUpstreamNginxConfig }; diff --git a/frontend/src/hooks/useUpstreams.ts b/frontend/src/hooks/useUpstreams.ts new file mode 100644 index 000000000..408c5ae3f --- /dev/null +++ b/frontend/src/hooks/useUpstreams.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getUpstreams, + HostsResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchUpstreams = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getUpstreams( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useUpstreams = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["upstreams", { offset, limit, sortBy, filters }], + queryFn: () => fetchUpstreams(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchUpstreams, useUpstreams }; diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts new file mode 100644 index 000000000..b41f5f457 --- /dev/null +++ b/frontend/src/hooks/useUser.ts @@ -0,0 +1,41 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +import { getUser, setUser, User } from "src/api/npm"; + +const fetchUser = (id: any) => { + return getUser(id, { expand: "capabilities" }); +}; + +const useUser = (id: string | number, options = {}) => { + return useQuery({ + queryKey: ["user", id], + queryFn: () => fetchUser(id), + staleTime: 60 * 1000, // 1 minute + ...options, + }); +}; + +const useSetUser = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (values: User) => setUser(values.id, values), + onMutate: (values) => { + const previousObject = queryClient.getQueryData(["user", values.id]); + + queryClient.setQueryData(["user", values.id], (old: any) => ({ + ...old, + ...values, + })); + + return () => + queryClient.setQueryData(["user", values.id], previousObject); + }, + onError: (_, __, rollback: any) => rollback(), + onSuccess: async ({ id }: User) => { + queryClient.invalidateQueries({ queryKey: ["user", id] }); + queryClient.invalidateQueries({ queryKey: ["users"] }); + }, + }); +}; + +export { useUser, useSetUser }; diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts new file mode 100644 index 000000000..26ec68e2a --- /dev/null +++ b/frontend/src/hooks/useUsers.ts @@ -0,0 +1,34 @@ +import { useQuery } from "@tanstack/react-query"; + +import { + getUsers, + UsersResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "src/api/npm"; + +const fetchUsers = (offset = 0, limit = 10, sortBy?: any, filters?: any) => { + return getUsers( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useUsers = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery({ + queryKey: ["users", { offset, limit, sortBy, filters }], + queryFn: () => fetchUsers(offset, limit, sortBy, filters), + staleTime: 15 * 1000, // 15 seconds + ...options, + }); +}; + +export { fetchUsers, useUsers }; diff --git a/frontend/src/index.scss b/frontend/src/index.scss new file mode 100644 index 000000000..3d0c14cb9 --- /dev/null +++ b/frontend/src/index.scss @@ -0,0 +1,77 @@ +@import "styles/fonts.scss"; + +html, +body, +#root { + margin: 0; + padding: 0; + height: 100%; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #f5f7fb; + color: rgb(73, 80, 87); +} + +.text-right { + text-align: right; +} + +table th.w-80, +table td.w-80 { + width: 80px; +} + +span.monospace { + font-family: monospace; +} + +pre { + &.wrappable { + white-space: pre-wrap; /* Since CSS 2.1 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + } + &.error { + color: rgb(245, 101, 101); /* red.500 */ + } +} + +/* helpdoc */ +.helpdoc-body { + p { + margin: 12px 0; + } + + ul, ol { + margin: 0 0 0 20px; + } + + h1 { + padding-inline-start: var(--chakra-space-6); + padding-inline-end: var(--chakra-space-6); + padding-top: var(--chakra-space-4); + padding-bottom: var(--chakra-space-4); + padding-left: 0; + border-bottom: 1px solid #aaa; + margin: 0; + font-size: var(--chakra-fontSizes-xl); + font-weight: var(--chakra-fontWeights-semibold); + } + + h2 { + padding-inline-start: var(--chakra-space-4); + padding-inline-end: var(--chakra-space-4); + padding-top: var(--chakra-space-4); + padding-left: 0; + margin: 0; + font-size: var(--chakra-fontSizes-lg); + font-weight: var(--chakra-fontWeights-semibold); + color: var(--chakra-colors-teal-500) + } +} diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx new file mode 100644 index 000000000..a239c3d5f --- /dev/null +++ b/frontend/src/locale/IntlProvider.tsx @@ -0,0 +1,75 @@ +import { createIntl, createIntlCache } from "react-intl"; + +import langDe from "./lang/de.json"; +import langEn from "./lang/en.json"; +import langFa from "./lang/fa.json"; +import langList from "./lang/lang-list.json"; + +// first item of each array should be the language code, +// not the country code +// Remember when adding to this list, also update check-locales.js script +const localeOptions = [ + ["en", "en-US"], + ["de", "de-DE"], + ["fa", "fa-IR"], +]; + +const loadMessages = (locale?: string): typeof langList & typeof langEn => { + locale = locale || "en"; + switch (locale.slice(0, 2)) { + case "de": + return Object.assign({}, langList, langEn, langDe); + case "fa": + return Object.assign({}, langList, langEn, langFa); + default: + return Object.assign({}, langList, langEn); + } +}; + +const getFlagCodeForLocale = (locale?: string) => { + switch (locale) { + case "de-DE": + case "de": + return "DE"; + case "fa-IR": + case "fa": + return "IR"; + default: + return "US"; + } +}; + +const getLocale = (short = false) => { + let loc = window.localStorage.getItem("locale"); + if (!loc) { + loc = document.documentElement.lang; + } + if (short) { + return loc.slice(0, 2); + } + return loc; +}; + +const cache = createIntlCache(); + +const initialMessages = loadMessages(getLocale()); +let intl = createIntl( + { locale: getLocale(), messages: initialMessages }, + cache, +); + +const changeLocale = (locale: string): void => { + const messages = loadMessages(locale); + intl = createIntl({ locale, messages }, cache); + window.localStorage.setItem("locale", locale); + document.documentElement.lang = locale; +}; + +export { + localeOptions, + getFlagCodeForLocale, + getLocale, + createIntl, + changeLocale, + intl, +}; diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts new file mode 100644 index 000000000..c6dca46d9 --- /dev/null +++ b/frontend/src/locale/index.ts @@ -0,0 +1 @@ +export * from "./IntlProvider"; diff --git a/frontend/src/locale/src/HelpDoc/de/AccessLists.md b/frontend/src/locale/src/HelpDoc/de/AccessLists.md new file mode 100644 index 000000000..d215e7ee1 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/AccessLists.md @@ -0,0 +1,3 @@ +# Access Lists Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/de/CertificateAuthorities.md b/frontend/src/locale/src/HelpDoc/de/CertificateAuthorities.md new file mode 100644 index 000000000..fbba8f06e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/CertificateAuthorities.md @@ -0,0 +1,29 @@ +# Certificate Authorities Help + +## What is a Certificate Authority? + +A **Certificate Authority (CA)**, also sometimes referred to as a +**Certification Authority**, is a company or organization that acts to validate +the identities of entities (such as websites, email addresses, companies, or +individual persons) and bind them to cryptographic keys through the issuance of +electronic documents known as digital certificates. + +## Which CA should I use? + +Not all CA's are created equal and you would be fine to use the default, +ZeroSSL. + +When using another CA it's worth considering the wildcard support and number of +hosts-per-certificate that it supports. + +## Can I use my own custom CA? + +Yes, you can run your own CA software. You would only do this if you have a +greater understanding of the SSL ecosystem. + +When requesting SSL Certificates through your custom CA and while they will be +successful, browsers will not automatically trust your CA and visiting hosts +using certificates issued by that CA will show errors. + +- [StepCA](https://smallstep.com/docs/step-ca) +- [Pebble](https://github.com/letsencrypt/pebble) diff --git a/frontend/src/locale/src/HelpDoc/de/Certificates.md b/frontend/src/locale/src/HelpDoc/de/Certificates.md new file mode 100644 index 000000000..de62492b0 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Certificates.md @@ -0,0 +1,37 @@ +# Certificates Help + +## HTTP Certificate + +A HTTP validated certificate means that the Certificate Authority (CA) will +attempt to reach your domains over HTTP (not HTTPS!) and if successful, the CA +will issue your certificate. + +For this method, you will have to have a _Host_ created for your domains(s) that +is accessible with HTTP. After a certificate has been given, you can modify the +_Host_ to also use this certificate for HTTPS connections. However, the _Host_ +will still need to be configured for HTTP access in order for the certificate to +renew. + +## DNS Certificate + +A DNS validated certificate requires you to create a DNS Provider. This DNS +Provider will be used to create temporary records on your domain and then the CA +will query those records to be sure you're the owner and if successful, the CA +will issue your certificate. + +You do not need a _Host_ to be created prior to requesting this type of +certificate. Nor do you need to have your _Host_ configured for HTTP access. + +## Custom Certificate + +Use this option to upload your own SSL Certificate, as provided by your own +Certificate Authority. + +## MKCert Certificate + +This option will create a self-signed Certificate for development use. When +viewing a _Host_ that using a MKCert Certificate, the browser will show errors. + +## Choosing a Certificate Authority + +If you're not sure, use **ZeroSSL.** diff --git a/frontend/src/locale/src/HelpDoc/de/DNSProviders.md b/frontend/src/locale/src/HelpDoc/de/DNSProviders.md new file mode 100644 index 000000000..b29316867 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/DNSProviders.md @@ -0,0 +1,14 @@ +# DNS Providers Help + +A DNS Provider is a service that hosts your domain(s) and this will be +specifically used when requesting new _Certificates_ via DNS Validation. + +It is preferred to use DNS Validation, so check if your domain is hosted with +one of the supported Acme.sh providers: + +- [DNS Page 1](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) +- [DNS Page 2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2) + +Should your DNS Provider be supported by Acme.sh and not available in this +project, please +[open an issue on Github](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new/choose). diff --git a/frontend/src/locale/src/HelpDoc/de/Hosts copy.md b/frontend/src/locale/src/HelpDoc/de/Hosts copy.md new file mode 100644 index 000000000..3e33b2a73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Hosts copy.md @@ -0,0 +1,3 @@ +# Hosts Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/de/Hosts.md b/frontend/src/locale/src/HelpDoc/de/Hosts.md new file mode 100644 index 000000000..3e33b2a73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Hosts.md @@ -0,0 +1,3 @@ +# Hosts Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/de/NginxTemplates.md b/frontend/src/locale/src/HelpDoc/de/NginxTemplates.md new file mode 100644 index 000000000..903837b63 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/NginxTemplates.md @@ -0,0 +1,3 @@ +# Nginx Templates Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/de/Upstreams.md b/frontend/src/locale/src/HelpDoc/de/Upstreams.md new file mode 100644 index 000000000..1a7f99fbf --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/Upstreams.md @@ -0,0 +1,3 @@ +# Upstreams Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/de/index.ts b/frontend/src/locale/src/HelpDoc/de/index.ts new file mode 100644 index 000000000..eda71d6de --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/de/index.ts @@ -0,0 +1,7 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as CertificateAuthorities from "./CertificateAuthorities.md"; +export * as DNSProviders from "./DNSProviders.md"; +export * as Hosts from "./Hosts.md"; +export * as NginxTemplates from "./NginxTemplates.md"; +export * as Upstreams from "./Upstreams.md"; diff --git a/frontend/src/locale/src/HelpDoc/en/AccessLists.md b/frontend/src/locale/src/HelpDoc/en/AccessLists.md new file mode 100644 index 000000000..d215e7ee1 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/AccessLists.md @@ -0,0 +1,3 @@ +# Access Lists Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md b/frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md new file mode 100644 index 000000000..fbba8f06e --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md @@ -0,0 +1,29 @@ +# Certificate Authorities Help + +## What is a Certificate Authority? + +A **Certificate Authority (CA)**, also sometimes referred to as a +**Certification Authority**, is a company or organization that acts to validate +the identities of entities (such as websites, email addresses, companies, or +individual persons) and bind them to cryptographic keys through the issuance of +electronic documents known as digital certificates. + +## Which CA should I use? + +Not all CA's are created equal and you would be fine to use the default, +ZeroSSL. + +When using another CA it's worth considering the wildcard support and number of +hosts-per-certificate that it supports. + +## Can I use my own custom CA? + +Yes, you can run your own CA software. You would only do this if you have a +greater understanding of the SSL ecosystem. + +When requesting SSL Certificates through your custom CA and while they will be +successful, browsers will not automatically trust your CA and visiting hosts +using certificates issued by that CA will show errors. + +- [StepCA](https://smallstep.com/docs/step-ca) +- [Pebble](https://github.com/letsencrypt/pebble) diff --git a/frontend/src/locale/src/HelpDoc/en/Certificates.md b/frontend/src/locale/src/HelpDoc/en/Certificates.md new file mode 100644 index 000000000..de62492b0 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Certificates.md @@ -0,0 +1,37 @@ +# Certificates Help + +## HTTP Certificate + +A HTTP validated certificate means that the Certificate Authority (CA) will +attempt to reach your domains over HTTP (not HTTPS!) and if successful, the CA +will issue your certificate. + +For this method, you will have to have a _Host_ created for your domains(s) that +is accessible with HTTP. After a certificate has been given, you can modify the +_Host_ to also use this certificate for HTTPS connections. However, the _Host_ +will still need to be configured for HTTP access in order for the certificate to +renew. + +## DNS Certificate + +A DNS validated certificate requires you to create a DNS Provider. This DNS +Provider will be used to create temporary records on your domain and then the CA +will query those records to be sure you're the owner and if successful, the CA +will issue your certificate. + +You do not need a _Host_ to be created prior to requesting this type of +certificate. Nor do you need to have your _Host_ configured for HTTP access. + +## Custom Certificate + +Use this option to upload your own SSL Certificate, as provided by your own +Certificate Authority. + +## MKCert Certificate + +This option will create a self-signed Certificate for development use. When +viewing a _Host_ that using a MKCert Certificate, the browser will show errors. + +## Choosing a Certificate Authority + +If you're not sure, use **ZeroSSL.** diff --git a/frontend/src/locale/src/HelpDoc/en/DNSProviders.md b/frontend/src/locale/src/HelpDoc/en/DNSProviders.md new file mode 100644 index 000000000..b29316867 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/DNSProviders.md @@ -0,0 +1,14 @@ +# DNS Providers Help + +A DNS Provider is a service that hosts your domain(s) and this will be +specifically used when requesting new _Certificates_ via DNS Validation. + +It is preferred to use DNS Validation, so check if your domain is hosted with +one of the supported Acme.sh providers: + +- [DNS Page 1](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) +- [DNS Page 2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2) + +Should your DNS Provider be supported by Acme.sh and not available in this +project, please +[open an issue on Github](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new/choose). diff --git a/frontend/src/locale/src/HelpDoc/en/Hosts copy.md b/frontend/src/locale/src/HelpDoc/en/Hosts copy.md new file mode 100644 index 000000000..3e33b2a73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Hosts copy.md @@ -0,0 +1,3 @@ +# Hosts Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/en/Hosts.md b/frontend/src/locale/src/HelpDoc/en/Hosts.md new file mode 100644 index 000000000..3e33b2a73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Hosts.md @@ -0,0 +1,3 @@ +# Hosts Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/en/NginxTemplates.md b/frontend/src/locale/src/HelpDoc/en/NginxTemplates.md new file mode 100644 index 000000000..903837b63 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/NginxTemplates.md @@ -0,0 +1,3 @@ +# Nginx Templates Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/en/Upstreams.md b/frontend/src/locale/src/HelpDoc/en/Upstreams.md new file mode 100644 index 000000000..1a7f99fbf --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/Upstreams.md @@ -0,0 +1,3 @@ +# Upstreams Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/en/index.ts b/frontend/src/locale/src/HelpDoc/en/index.ts new file mode 100644 index 000000000..eda71d6de --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/en/index.ts @@ -0,0 +1,7 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as CertificateAuthorities from "./CertificateAuthorities.md"; +export * as DNSProviders from "./DNSProviders.md"; +export * as Hosts from "./Hosts.md"; +export * as NginxTemplates from "./NginxTemplates.md"; +export * as Upstreams from "./Upstreams.md"; diff --git a/frontend/src/locale/src/HelpDoc/fa/AccessLists.md b/frontend/src/locale/src/HelpDoc/fa/AccessLists.md new file mode 100644 index 000000000..d215e7ee1 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/AccessLists.md @@ -0,0 +1,3 @@ +# Access Lists Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/fa/CertificateAuthorities.md b/frontend/src/locale/src/HelpDoc/fa/CertificateAuthorities.md new file mode 100644 index 000000000..7f8186a50 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/CertificateAuthorities.md @@ -0,0 +1,28 @@ +# راهنمایی‌های مربوط به مراجع صادرکننده گواهینامه + +## مرجع صادرکننده گواهینامه چیست؟ + +مرجع صادرکننده گواهینامه (CA) یا همان مرجع تأیید شناسه، یک شرکت یا سازمان است که +به منظور تأیید هویت انجام‌دهندگان (مانند وب سایت‌ها، آدرس‌های ایمیل، شرکت‌ها یا +افراد) و اتصال آن‌ها به کلیدهای رمزنگاری، از طریق صدور سند الکترونیکی به نام +گواهینامه دیجیتال، فعالیت می‌کند. + +## از کدام مرجع صادرکننده گواهینامه باید استفاده کنم؟ + +همه مراجع صادرکننده گواهینامه‌ها برابر نیستند و شما می‌توانید از پیش‌فرض ZeroSSL +استفاده کنید. + +در هنگام استفاده از یک مرجع صادرکننده گواهینامه دیگر، ارزش پشتیبانی از ویلدکارد +و تعداد میزبان‌ها در هر گواهینامه را در نظر بگیرید. + +## آیا می‌توانم از مرجع صادرکننده گواهینامه شخصی خود استفاده کنم؟ + +بله، می‌توانید نرم‌افزار CA خود را اجرا کنید. این کار را فقط در صورت داشتن درک +بیشتری از بخش SSL انجام دهید. + +هنگام درخواست گواهینامه SSL از CA شخصی خود، درخواست‌های شما موفق خواهند بود، اما +مرورگرها به طور خودکار به CA شما اعتماد نخواهند کرد و میزبان‌های با استفاده از +گواهینامه‌های صادر شده توسط آن CA، خطاها را نمایش می‌دهند. + +- [StepCA](https://smallstep.com/docs/step-ca) +- [Pebble](https://github.com/letsencrypt/pebble) diff --git a/frontend/src/locale/src/HelpDoc/fa/Certificates.md b/frontend/src/locale/src/HelpDoc/fa/Certificates.md new file mode 100644 index 000000000..48d389b41 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/Certificates.md @@ -0,0 +1,37 @@ +# راهنمایی گواهی‌نامه‌ها + +## گواهینامه HTTP + +گواهینامه معتبرساز (CA) HTTP به معنای آن است که CA سعی خواهد کرد تا به دامنه های +شما به صورت HTTP (نه HTTPS!) دسترسی پیدا کند و در صورت موفقیت، گواهینامه شما را +صادر خواهد کرد. + +برای این روش، شما باید یک میزبان (Host) برای دامنه (های) خود ایجاد کنید که با +HTTP قابل دسترسی باشد. پس از صدور گواهینامه، شما می توانید میزبان (Host) خود را +به گواهینامه خود برای اتصالات HTTPS نیز تنظیم کنید. با این حال، میزبان (Host) +همچنان باید برای دسترسی HTTP پیکربندی شود تا گواهینامه تجدید شود. + +## گواهینامه DNS + +گواهینامه معتبرساز DNS نیازمند ایجاد یک ارائه دهنده DNS است. این ارائه دهنده DNS +برای ایجاد رکوردهای موقت در دامنه شما استفاده خواهد شد و سپس CA برای اطمینان از +اینکه شما مالک آن هستید، این رکوردها را پرس‌وجو خواهد کرد و در صورت موفقیت، +گواهینامه شما را صادر خواهد کرد. + +شما نیازی به ایجاد میزبان (Host) قبل از درخواست این نوع گواهینامه ندارید. +همچنین، شما نیازی به پیکربندی میزبان (Host) خود برای دسترسی HTTP ندارید. + +## گواهینامه سفارشی + +از این گزینه برای بارگذاری گواهینامه SSL خود، به عنوان ارائه شده توسط معتبرساز +گواهینامه خود، استفاده کنید. + +## گواهینامه MKCert + +این گزینه یک گواهینامه امضای دیجیتالی خود امضا شده برای استفاده در محیط توسعه +ایجاد می‌کند. هنگام مشاهده یک میزبانی (Host) که از گواهینامه MKCert استفاده +می‌کند، مرورگر خطاها را نشان می‌دهد. + +## انتخاب یک مرجع اعتبار سنجی + +اگر مطمئن نیستید، از ZeroSSL استفاده کنید. diff --git a/frontend/src/locale/src/HelpDoc/fa/DNSProviders.md b/frontend/src/locale/src/HelpDoc/fa/DNSProviders.md new file mode 100644 index 000000000..b29316867 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/DNSProviders.md @@ -0,0 +1,14 @@ +# DNS Providers Help + +A DNS Provider is a service that hosts your domain(s) and this will be +specifically used when requesting new _Certificates_ via DNS Validation. + +It is preferred to use DNS Validation, so check if your domain is hosted with +one of the supported Acme.sh providers: + +- [DNS Page 1](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) +- [DNS Page 2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2) + +Should your DNS Provider be supported by Acme.sh and not available in this +project, please +[open an issue on Github](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new/choose). diff --git a/frontend/src/locale/src/HelpDoc/fa/Hosts.md b/frontend/src/locale/src/HelpDoc/fa/Hosts.md new file mode 100644 index 000000000..3e33b2a73 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/Hosts.md @@ -0,0 +1,3 @@ +# Hosts Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/fa/NginxTemplates.md b/frontend/src/locale/src/HelpDoc/fa/NginxTemplates.md new file mode 100644 index 000000000..903837b63 --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/NginxTemplates.md @@ -0,0 +1,3 @@ +# Nginx Templates Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/fa/Upstreams.md b/frontend/src/locale/src/HelpDoc/fa/Upstreams.md new file mode 100644 index 000000000..1a7f99fbf --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/Upstreams.md @@ -0,0 +1,3 @@ +# Upstreams Help + +todo diff --git a/frontend/src/locale/src/HelpDoc/fa/index.ts b/frontend/src/locale/src/HelpDoc/fa/index.ts new file mode 100644 index 000000000..eda71d6de --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/fa/index.ts @@ -0,0 +1,7 @@ +export * as AccessLists from "./AccessLists.md"; +export * as Certificates from "./Certificates.md"; +export * as CertificateAuthorities from "./CertificateAuthorities.md"; +export * as DNSProviders from "./DNSProviders.md"; +export * as Hosts from "./Hosts.md"; +export * as NginxTemplates from "./NginxTemplates.md"; +export * as Upstreams from "./Upstreams.md"; diff --git a/frontend/src/locale/src/HelpDoc/index.ts b/frontend/src/locale/src/HelpDoc/index.ts new file mode 100644 index 000000000..3bb6d93cb --- /dev/null +++ b/frontend/src/locale/src/HelpDoc/index.ts @@ -0,0 +1,17 @@ +import * as de from "./de/index"; +import * as en from "./en/index"; +import * as fa from "./fa/index"; + +const items: any = { de, en, fa }; + +export const getHelpFile = (lang: string, section: string): string => { + if ( + typeof items[lang] !== "undefined" && + typeof items[lang][section] !== "undefined" + ) { + return items[lang][section].default; + } + throw new Error(`Cannot load help doc for ${lang}-${section}`); +}; + +export default items; diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json new file mode 100644 index 000000000..319c93ac9 --- /dev/null +++ b/frontend/src/locale/src/de.json @@ -0,0 +1,461 @@ +{ + "access-list.create": { + "defaultMessage": "Create Access List" + }, + "access-lists.title": { + "defaultMessage": "Zugriffslisten" + }, + "action.delete": { + "defaultMessage": "Delete" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "Bearbeiten" + }, + "action.nginx-config": { + "defaultMessage": "View Nginx Config" + }, + "action.renew": { + "defaultMessage": "Renew" + }, + "action.set-password": { + "defaultMessage": "Passwort festlegen" + }, + "audit-log.title": { + "defaultMessage": "Audit-Protokoll" + }, + "brand.name": { + "defaultMessage": "Nginx Proxy Manager" + }, + "capability-count": { + "defaultMessage": "{count} Artikel" + }, + "capability.full-admin": { + "defaultMessage": "Vollständiger Administrator" + }, + "capability.hosts.view": { + "defaultMessage": "Gastgeber anzeigen" + }, + "capability.system": { + "defaultMessage": "System" + }, + "capability.users.manage": { + "defaultMessage": "Benutzer verwalten" + }, + "certificate-authorities.title": { + "defaultMessage": "Zertifizierungsstellen" + }, + "certificate-authority": { + "defaultMessage": "Certificate Authority" + }, + "certificate-authority.acmesh-server": { + "defaultMessage": "ACME-Server" + }, + "certificate-authority.ca-bundle": { + "defaultMessage": "CA-Zertifikatpaket" + }, + "certificate-authority.create": { + "defaultMessage": "Zertifizierungsstelle erstellen" + }, + "certificate-authority.edit": { + "defaultMessage": "Zertifizierungsstelle bearbeiten" + }, + "certificate-authority.has-wildcard-support": { + "defaultMessage": "Hat Wildcard-Unterstützung" + }, + "certificate-authority.max-domains": { + "defaultMessage": "Maximale Domains pro Zertifikat" + }, + "certificate.create": { + "defaultMessage": "Zertifikat erstellen" + }, + "certificate.deleted": { + "defaultMessage": "Certificate has been deleted" + }, + "certificate.renewal-requested": { + "defaultMessage": "Renewal has been queued" + }, + "certificates.title": { + "defaultMessage": "Zertifikate" + }, + "change-password": { + "defaultMessage": "Passwort ändern" + }, + "column.acmesh-name": { + "defaultMessage": "Acme.sh-Plugin" + }, + "column.description": { + "defaultMessage": "Beschreibung" + }, + "column.dns-sleep": { + "defaultMessage": "DNS-Schlaf" + }, + "column.domain-names": { + "defaultMessage": "Domänen" + }, + "column.id": { + "defaultMessage": "ICH WÜRDE" + }, + "column.max-domains": { + "defaultMessage": "Domänen pro Zertifikat" + }, + "column.servers": { + "defaultMessage": "Servers" + }, + "column.status": { + "defaultMessage": "Status" + }, + "column.type": { + "defaultMessage": "Art" + }, + "column.validation-type": { + "defaultMessage": "Validierung" + }, + "column.wildcard-support": { + "defaultMessage": "Wildcard-Unterstützung" + }, + "create-access-list-title": { + "defaultMessage": "Create Access List" + }, + "create-certificate-title": { + "defaultMessage": "Es gibt keine Zertifikate" + }, + "create-dns-provider": { + "defaultMessage": "Erstellen Sie einen DNS-Anbieter" + }, + "create-dns-provider-title": { + "defaultMessage": "Es gibt keine DNS-Anbieter" + }, + "create-hint": { + "defaultMessage": "Warum erstellen Sie nicht eine?" + }, + "create-host-title": { + "defaultMessage": "Es gibt keine Proxy-Hosts" + }, + "create-nginx-template": { + "defaultMessage": "Nginxvorlage erstellen" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, + "dashboard.title": { + "defaultMessage": "Armaturenbrett" + }, + "disabled": { + "defaultMessage": "Deaktiviert" + }, + "dns-provider": { + "defaultMessage": "DNS Provider" + }, + "dns-provider.acmesh-name": { + "defaultMessage": "Acme.sh-Name" + }, + "dns-provider.create": { + "defaultMessage": "Erstellen Sie einen DNS-Anbieter" + }, + "dns-providers.empty": { + "defaultMessage": "No DNS Providers - Create one first" + }, + "domain_names": { + "defaultMessage": "Domain Names" + }, + "domain_names.max": { + "defaultMessage": "{count} domain names maximum" + }, + "dns-providers.title": { + "defaultMessage": "DNS-Anbieter" + }, + "error": { + "defaultMessage": "Fehler" + }, + "error.ca-bundle-does-not-exist": { + "defaultMessage": "Datei existiert nicht auf dem Server" + }, + "error.cannot-save-system-users": { + "defaultMessage": "Systembenutzer können nicht geändert werden" + }, + "error.current-password-invalid": { + "defaultMessage": "Aktuelles Passwort ist ungültig" + }, + "error.database-unavailable": { + "defaultMessage": "Datenbank ist nicht verfügbar" + }, + "error.email-already-exists": { + "defaultMessage": "Es existiert bereits ein Benutzer mit dieser E-Mail-Adresse" + }, + "error.invalid-auth-type": { + "defaultMessage": "Invalid authentication type" + }, + "error.invalid-login-credentials": { + "defaultMessage": "Ungültige Login-Details" + }, + "error.provider_not_found": { + "defaultMessage": "Provider not found" + }, + "error.request-failed-validation": { + "defaultMessage": "Back-End-Validierung fehlgeschlagen" + }, + "error.user-disabled": { + "defaultMessage": "Konto ist deaktiviert" + }, + "filter.apply": { + "defaultMessage": "Anwenden" + }, + "filter.clear": { + "defaultMessage": "Klar" + }, + "filter.contains": { + "defaultMessage": "Enthält" + }, + "filter.ends": { + "defaultMessage": "Endet mit" + }, + "filter.exactly": { + "defaultMessage": "Exakt" + }, + "filter.placeholder": { + "defaultMessage": "Suchbegriff eingeben" + }, + "filter.starts": { + "defaultMessage": "Beginnt mit" + }, + "footer.changelog": { + "defaultMessage": "Änderungen" + }, + "footer.copyright": { + "defaultMessage": "Copyright © {year} jc21.com" + }, + "footer.github": { + "defaultMessage": "Github" + }, + "footer.userguide": { + "defaultMessage": "Handbuch" + }, + "form.cancel": { + "defaultMessage": "Stornieren" + }, + "form.invalid-email": { + "defaultMessage": "Ungültige E-Mail-Adresse" + }, + "form.max-int": { + "defaultMessage": "Das Maximum ist {count}" + }, + "form.max-length": { + "defaultMessage": "Maximum length is {count, plural, one {# character} other {# characters}}" + }, + "form.min-int": { + "defaultMessage": "Das Minimum ist {count}" + }, + "form.min-length": { + "defaultMessage": "Minimum length is {count, plural, one {# character} other {# characters}}" + }, + "form.required": { + "defaultMessage": "Dies ist erforderlich" + }, + "form.save": { + "defaultMessage": "Speichern" + }, + "full-access": { + "defaultMessage": "Voller Zugriff" + }, + "full-access.description": { + "defaultMessage": "Zugriff auf alle Funktionen" + }, + "general-settings.title": { + "defaultMessage": "Allgemeine Einstellungen" + }, + "host.create": { + "defaultMessage": "Host erstellen" + }, + "hosts.title": { + "defaultMessage": "Gastgeber" + }, + "http-https": { + "defaultMessage": "HTTP/HTTPS" + }, + "http-only": { + "defaultMessage": "Nur HTTP" + }, + "https-only": { + "defaultMessage": "Nur HTTPS" + }, + "is-ecc": { + "defaultMessage": "ECC Certificate" + }, + "lets-go": { + "defaultMessage": "Lass uns gehen" + }, + "login.login": { + "defaultMessage": "Einloggen" + }, + "name": { + "defaultMessage": "Name" + }, + "navigation.close": { + "defaultMessage": "Navigation schließen" + }, + "navigation.open": { + "defaultMessage": "Navigation öffnen" + }, + "nginx-config": { + "defaultMessage": "Nginx Config" + }, + "nginx-templates.title": { + "defaultMessage": "nginx-Vorlagen" + }, + "no-access": { + "defaultMessage": "Kein Zugang" + }, + "password.confirm": { + "defaultMessage": "Bestätige neues Passwort" + }, + "password.current": { + "defaultMessage": "Aktuelles Passwort" + }, + "password.new": { + "defaultMessage": "Neues Kennwort" + }, + "permissions.title": { + "defaultMessage": "Berechtigungen" + }, + "profile.logout": { + "defaultMessage": "Ausloggen" + }, + "profile.title": { + "defaultMessage": "Profileinstellungen" + }, + "restricted-access": { + "defaultMessage": "Eingeschränkter Zugang" + }, + "restricted-access.description": { + "defaultMessage": "Passen Sie die Berechtigungen für diesen Benutzer an" + }, + "seconds": { + "defaultMessage": "{seconds} Sekunden" + }, + "set-password": { + "defaultMessage": "Passwort festlegen" + }, + "settings.title": { + "defaultMessage": "Einstellungen" + }, + "setup.create": { + "defaultMessage": "Registrieren" + }, + "setup.title": { + "defaultMessage": "Erstelle deinen ersten Account" + }, + "ssl.title": { + "defaultMessage": "SSL" + }, + "status.failed": { + "defaultMessage": "Failed" + }, + "status.ok": { + "defaultMessage": "OK" + }, + "status.ready": { + "defaultMessage": "Bereit" + }, + "tables.clear-all-filters": { + "defaultMessage": "{count, plural, one {Filter löschen} other {Löschen Sie # Filter}}" + }, + "tables.no-items": { + "defaultMessage": "Es sind keine Artikel vorhandenThere are no items" + }, + "tables.no-items-with-filters": { + "defaultMessage": "Es gibt keine Elemente, die {count, plural, one {diesem Filter} other {diesen Filtern}} entsprechen" + }, + "tables.pagination-counts": { + "defaultMessage": "Showing {start} to {end} of {total, plural, =0 {no items} one {# item} other {# items}}" + }, + "tables.pagination-next": { + "defaultMessage": "Nächste Seite" + }, + "tables.pagination-previous": { + "defaultMessage": "Vorherige Seite" + }, + "tables.pagination-select": { + "defaultMessage": "Wählen Sie eine Seite aus" + }, + "theme.to-dark": { + "defaultMessage": "Wechseln Sie zum dunklen Design" + }, + "theme.to-light": { + "defaultMessage": "Wechseln Sie zum Lichtdesign" + }, + "type.custom": { + "defaultMessage": "Custom" + }, + "type.dead": { + "defaultMessage": "404 Host" + }, + "type.dns": { + "defaultMessage": "DNS" + }, + "type.http": { + "defaultMessage": "HTTP" + }, + "type.mkcert": { + "defaultMessage": "MKCert" + }, + "type.proxy": { + "defaultMessage": "Proxy Host" + }, + "type.redirect": { + "defaultMessage": "Redirection" + }, + "type.stream": { + "defaultMessage": "Stream" + }, + "type.upstream": { + "defaultMessage": "Upstream" + }, + "unhealthy.body": { + "defaultMessage": "Wir werden weiterhin den Zustand überprüfen und hoffen, bald wieder einsatzbereit zu sein!" + }, + "unhealthy.title": { + "defaultMessage": "Nginx Proxy Manager ist fehlerhaft" + }, + "upstream.create": { + "defaultMessage": "Create Upstream" + }, + "upstreams.title": { + "defaultMessage": "Upstreams" + }, + "user.capabilities": { + "defaultMessage": "Fähigkeiten" + }, + "user.create": { + "defaultMessage": "Benutzer erstellen" + }, + "user.disabled": { + "defaultMessage": "Benutzer ist deaktiviert" + }, + "user.edit": { + "defaultMessage": "Benutzer bearbeiten" + }, + "user.email": { + "defaultMessage": "E-Mail" + }, + "user.name": { + "defaultMessage": "Name" + }, + "user.password": { + "defaultMessage": "Passwort" + }, + "users.title": { + "defaultMessage": "Benutzer" + }, + "view-only": { + "defaultMessage": "Nur anschauen" + }, + "wildcards-not-permitted": { + "defaultMessage": "Wildcards not permitted for this type" + }, + "wildcards-not-supported": { + "defaultMessage": "Wildcards not supported for this CA" + } +} diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json new file mode 100644 index 000000000..00573e085 --- /dev/null +++ b/frontend/src/locale/src/en.json @@ -0,0 +1,743 @@ +{ + "access-list.create": { + "defaultMessage": "Create Access List" + }, + "access-lists.title": { + "defaultMessage": "Access Lists" + }, + "acmesh-property.access-key-id": { + "defaultMessage": "Access Key ID" + }, + "acmesh-property.account-id": { + "defaultMessage": "Account ID" + }, + "acmesh-property.api-key": { + "defaultMessage": "API Key" + }, + "acmesh-property.api-url": { + "defaultMessage": "API URL" + }, + "acmesh-property.app-id": { + "defaultMessage": "APP ID" + }, + "acmesh-property.auth-id": { + "defaultMessage": "Auth ID" + }, + "acmesh-property.base-url": { + "defaultMessage": "Base URL" + }, + "acmesh-property.client-id": { + "defaultMessage": "Client ID" + }, + "acmesh-property.client-secret": { + "defaultMessage": "Client Secret" + }, + "acmesh-property.context": { + "defaultMessage": "Context" + }, + "acmesh-property.credentials": { + "defaultMessage": "Credentials" + }, + "acmesh-property.customer": { + "defaultMessage": "Customer" + }, + "acmesh-property.email": { + "defaultMessage": "Email" + }, + "acmesh-property.id": { + "defaultMessage": "ID" + }, + "acmesh-property.insecure": { + "defaultMessage": "Insecure" + }, + "acmesh-property.key": { + "defaultMessage": "Key" + }, + "acmesh-property.oauth-token": { + "defaultMessage": "OAuth Token" + }, + "acmesh-property.otp-secret": { + "defaultMessage": "OTP Secret" + }, + "acmesh-property.password": { + "defaultMessage": "Password" + }, + "acmesh-property.secret": { + "defaultMessage": "Secret" + }, + "acmesh-property.secret-access-key": { + "defaultMessage": "Secret Access Key" + }, + "acmesh-property.server": { + "defaultMessage": "Server" + }, + "acmesh-property.server-id": { + "defaultMessage": "Server ID" + }, + "acmesh-property.slow-rate": { + "defaultMessage": "Slow Rate" + }, + "acmesh-property.sub-auth-id": { + "defaultMessage": "Sub-Auth ID" + }, + "acmesh-property.subdomain": { + "defaultMessage": "Subdomain" + }, + "acmesh-property.subscription-id": { + "defaultMessage": "Subscription ID" + }, + "acmesh-property.tenant-id": { + "defaultMessage": "Tenant ID" + }, + "acmesh-property.token": { + "defaultMessage": "Token" + }, + "acmesh-property.ttl": { + "defaultMessage": "TTL" + }, + "acmesh-property.url": { + "defaultMessage": "URL" + }, + "acmesh-property.user": { + "defaultMessage": "User" + }, + "acmesh-property.username": { + "defaultMessage": "Username" + }, + "acmesh-property.zone-id": { + "defaultMessage": "Zone ID" + }, + "acmesh.dns_acmedns": { + "defaultMessage": "ACME DNS" + }, + "acmesh.dns_ad": { + "defaultMessage": "Alwaysdata" + }, + "acmesh.dns_ali": { + "defaultMessage": "Aliyun" + }, + "acmesh.dns_autodns": { + "defaultMessage": "autoDNS (InternetX)" + }, + "acmesh.dns_aws": { + "defaultMessage": "AWS Route53" + }, + "acmesh.dns_azure": { + "defaultMessage": "Azure" + }, + "acmesh.dns_cf": { + "defaultMessage": "Cloudflare" + }, + "acmesh.dns_cloudns": { + "defaultMessage": "ClouDNS.net" + }, + "acmesh.dns_conoha": { + "defaultMessage": "Conoha" + }, + "acmesh.dns_cx": { + "defaultMessage": "CloudXNS" + }, + "acmesh.dns_cyon": { + "defaultMessage": "Cyon.ch" + }, + "acmesh.dns_da": { + "defaultMessage": "DirectAdmin" + }, + "acmesh.dns_dgon": { + "defaultMessage": "DigitalOcean" + }, + "acmesh.dns_dnsimple": { + "defaultMessage": "DNSimple" + }, + "acmesh.dns_dp": { + "defaultMessage": "DNSPod.cn" + }, + "acmesh.dns_dpi": { + "defaultMessage": "DNSPod.com" + }, + "acmesh.dns_dreamhost": { + "defaultMessage": "DreamHost" + }, + "acmesh.dns_duckdns": { + "defaultMessage": "DuckDNS" + }, + "acmesh.dns_dyn": { + "defaultMessage": "Dyn" + }, + "acmesh.dns_dynu": { + "defaultMessage": "Dynu" + }, + "acmesh.dns_euserv": { + "defaultMessage": "Euserv" + }, + "acmesh.dns_freedns": { + "defaultMessage": "FreeDNS" + }, + "acmesh.dns_gandi_livedns": { + "defaultMessage": "Gandi LiveDNS" + }, + "acmesh.dns_gd": { + "defaultMessage": "GoDaddy" + }, + "acmesh.dns_he": { + "defaultMessage": "Hurricane Electric" + }, + "acmesh.dns_infoblox": { + "defaultMessage": "Infoblox" + }, + "acmesh.dns_inwx": { + "defaultMessage": "INWX" + }, + "acmesh.dns_ispconfig": { + "defaultMessage": "ISPConfig" + }, + "acmesh.dns_kinghost": { + "defaultMessage": "KingHost" + }, + "acmesh.dns_linode_v4": { + "defaultMessage": "Linode" + }, + "acmesh.dns_loopia": { + "defaultMessage": "Loopia" + }, + "acmesh.dns_lua": { + "defaultMessage": "LuaDNS" + }, + "acmesh.dns_me": { + "defaultMessage": "DNSMadeEasy" + }, + "acmesh.dns_namecom": { + "defaultMessage": "Name.com" + }, + "acmesh.dns_namesilo": { + "defaultMessage": "Namesilo.com" + }, + "acmesh.dns_nsone": { + "defaultMessage": "NS1.com" + }, + "acmesh.dns_pdns": { + "defaultMessage": "PowerDNS" + }, + "acmesh.dns_selectel": { + "defaultMessage": "Selectel" + }, + "acmesh.dns_servercow": { + "defaultMessage": "Servercow" + }, + "acmesh.dns_tele3": { + "defaultMessage": "TELE3" + }, + "acmesh.dns_unoeuro": { + "defaultMessage": "UnoEuro" + }, + "acmesh.dns_vscale": { + "defaultMessage": "VSCALE" + }, + "acmesh.dns_yandex": { + "defaultMessage": "pdd.yandex.ru" + }, + "acmesh.dns_zilore": { + "defaultMessage": "Zilore" + }, + "acmesh.dns_zonomi": { + "defaultMessage": "Zonomi" + }, + "action.delete": { + "defaultMessage": "Delete" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "Edit" + }, + "action.nginx-config": { + "defaultMessage": "View Nginx Config" + }, + "action.renew": { + "defaultMessage": "Renew" + }, + "action.set-password": { + "defaultMessage": "Set Password" + }, + "audit-log.title": { + "defaultMessage": "Audit Log" + }, + "brand.name": { + "defaultMessage": "Nginx Proxy Manager" + }, + "capability-count": { + "defaultMessage": "{count} items" + }, + "capability.access-lists.manage": { + "defaultMessage": "Manage Access Lists" + }, + "capability.access-lists.view": { + "defaultMessage": "View Access Lists" + }, + "capability.audit-log.view": { + "defaultMessage": "View Audit Log" + }, + "capability.certificate-authorities.manage": { + "defaultMessage": "Manage Certificate Authorities" + }, + "capability.certificate-authorities.view": { + "defaultMessage": "View Certificate Authorities" + }, + "capability.certificates.manage": { + "defaultMessage": "Manage Certificates" + }, + "capability.certificates.view": { + "defaultMessage": "View Certificates" + }, + "capability.dns-providers.manage": { + "defaultMessage": "Manage DNS Providers" + }, + "capability.dns-providers.view": { + "defaultMessage": "View DNS Providers" + }, + "capability.full-admin": { + "defaultMessage": "Full Admin" + }, + "capability.hosts.manage": { + "defaultMessage": "Manage Hosts" + }, + "capability.hosts.view": { + "defaultMessage": "View Hosts" + }, + "capability.nginx-templates.manage": { + "defaultMessage": "Manage Nginx Templates" + }, + "capability.nginx-templates.view": { + "defaultMessage": "View Nginx Templates" + }, + "capability.settings.manage": { + "defaultMessage": "Manage Settings" + }, + "capability.system": { + "defaultMessage": "System" + }, + "capability.users.manage": { + "defaultMessage": "Manage Users" + }, + "certificate-authorities.title": { + "defaultMessage": "Certificate Authorities" + }, + "certificate-authority": { + "defaultMessage": "Certificate Authority" + }, + "certificate-authority.acmesh-server": { + "defaultMessage": "ACME Server" + }, + "certificate-authority.ca-bundle": { + "defaultMessage": "CA Certificate Bundle" + }, + "certificate-authority.create": { + "defaultMessage": "Create Certificate Authority" + }, + "certificate-authority.edit": { + "defaultMessage": "Edit Certificate Authority" + }, + "certificate-authority.has-wildcard-support": { + "defaultMessage": "Has Wildcard Support" + }, + "certificate-authority.max-domains": { + "defaultMessage": "Maximum Domains per Certificate" + }, + "certificate.create": { + "defaultMessage": "Create Certificate" + }, + "certificate.deleted": { + "defaultMessage": "Certificate has been deleted" + }, + "certificate.renewal-requested": { + "defaultMessage": "Renewal has been queued" + }, + "certificates.title": { + "defaultMessage": "Certificates" + }, + "change-password": { + "defaultMessage": "Change Password" + }, + "column.acmesh-name": { + "defaultMessage": "Acme.sh Provider" + }, + "column.description": { + "defaultMessage": "Description" + }, + "column.dns-sleep": { + "defaultMessage": "DNS Sleep" + }, + "column.domain-names": { + "defaultMessage": "Domains" + }, + "column.id": { + "defaultMessage": "ID" + }, + "column.max-domains": { + "defaultMessage": "Domains per Cert" + }, + "column.servers": { + "defaultMessage": "Servers" + }, + "column.status": { + "defaultMessage": "Status" + }, + "column.type": { + "defaultMessage": "Type" + }, + "column.validation-type": { + "defaultMessage": "Validation" + }, + "column.wildcard-support": { + "defaultMessage": "Wildcard Support" + }, + "create-access-list-title": { + "defaultMessage": "There are no Access Lists" + }, + "create-certificate-title": { + "defaultMessage": "There are no Certificates" + }, + "create-dns-provider": { + "defaultMessage": "Create DNS Provider" + }, + "create-dns-provider-title": { + "defaultMessage": "There are no DNS Providers" + }, + "create-hint": { + "defaultMessage": "Why don't you create one?" + }, + "create-host-title": { + "defaultMessage": "There are no Hosts" + }, + "create-nginx-template": { + "defaultMessage": "Create Nginx Template" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, + "dashboard.title": { + "defaultMessage": "Dashboard" + }, + "disabled": { + "defaultMessage": "Disabled" + }, + "dns-provider": { + "defaultMessage": "DNS Provider" + }, + "dns-provider.acmesh-name": { + "defaultMessage": "Acme.sh Provider" + }, + "dns-provider.create": { + "defaultMessage": "Create DNS Provider" + }, + "dns-providers.empty": { + "defaultMessage": "No DNS Providers - Create one first" + }, + "domain_names": { + "defaultMessage": "Domain Names" + }, + "domain_names.max": { + "defaultMessage": "{count} domain names maximum" + }, + "dns-providers.title": { + "defaultMessage": "DNS Providers" + }, + "error": { + "defaultMessage": "Error" + }, + "error.ca-bundle-does-not-exist": { + "defaultMessage": "File doesn't exist on the server" + }, + "error.cannot-save-system-users": { + "defaultMessage": "You cannot modify system users" + }, + "error.current-password-invalid": { + "defaultMessage": "Current password is invalid" + }, + "error.database-unavailable": { + "defaultMessage": "Database is unavailable" + }, + "error.email-already-exists": { + "defaultMessage": "A user already exists with this email address" + }, + "error.invalid-auth-type": { + "defaultMessage": "Invalid authentication type" + }, + "error.invalid-login-credentials": { + "defaultMessage": "Invalid login credentials" + }, + "error.provider_not_found": { + "defaultMessage": "Provider not found" + }, + "error.request-failed-validation": { + "defaultMessage": "Failed backend validation" + }, + "error.user-disabled": { + "defaultMessage": "Account is disabled" + }, + "filter.apply": { + "defaultMessage": "Apply" + }, + "filter.clear": { + "defaultMessage": "Clear" + }, + "filter.contains": { + "defaultMessage": "Contains" + }, + "filter.ends": { + "defaultMessage": "Ends with" + }, + "filter.exactly": { + "defaultMessage": "Exactly" + }, + "filter.placeholder": { + "defaultMessage": "Enter search query" + }, + "filter.starts": { + "defaultMessage": "Begins with" + }, + "footer.changelog": { + "defaultMessage": "Change Log" + }, + "footer.copyright": { + "defaultMessage": "Copyright © {year} jc21.com" + }, + "footer.github": { + "defaultMessage": "Github" + }, + "footer.userguide": { + "defaultMessage": "User Guide" + }, + "form.cancel": { + "defaultMessage": "Cancel" + }, + "form.invalid-email": { + "defaultMessage": "Invalid email address" + }, + "form.max-int": { + "defaultMessage": "Maximum is {count}" + }, + "form.max-length": { + "defaultMessage": "Maximum length is {count, plural, one {# character} other {# characters}}" + }, + "form.min-int": { + "defaultMessage": "Minimum is {count}" + }, + "form.min-length": { + "defaultMessage": "Minimum length is {count, plural, one {# character} other {# characters}}" + }, + "form.required": { + "defaultMessage": "This is required" + }, + "form.save": { + "defaultMessage": "Save" + }, + "full-access": { + "defaultMessage": "Full Access" + }, + "full-access.description": { + "defaultMessage": "Access to all functionality" + }, + "general-settings.title": { + "defaultMessage": "General Settings" + }, + "host.create": { + "defaultMessage": "Create Host" + }, + "hosts.title": { + "defaultMessage": "Hosts" + }, + "http-https": { + "defaultMessage": "HTTP/HTTPS" + }, + "http-only": { + "defaultMessage": "HTTP Only" + }, + "https-only": { + "defaultMessage": "HTTPS Only" + }, + "is-ecc": { + "defaultMessage": "ECC Certificate" + }, + "lets-go": { + "defaultMessage": "Let's go" + }, + "login.login": { + "defaultMessage": "Sign in" + }, + "name": { + "defaultMessage": "Name" + }, + "navigation.close": { + "defaultMessage": "Close navigation" + }, + "navigation.open": { + "defaultMessage": "Open navigation" + }, + "nginx-config": { + "defaultMessage": "Nginx Config" + }, + "nginx-templates.title": { + "defaultMessage": "Nginx Templates" + }, + "no-access": { + "defaultMessage": "No Access" + }, + "password.confirm": { + "defaultMessage": "Confirm New Password" + }, + "password.current": { + "defaultMessage": "Current Password" + }, + "password.new": { + "defaultMessage": "New Password" + }, + "permissions.title": { + "defaultMessage": "Permissions" + }, + "profile.logout": { + "defaultMessage": "Logout" + }, + "profile.title": { + "defaultMessage": "Profile" + }, + "restricted-access": { + "defaultMessage": "Restricted Access" + }, + "restricted-access.description": { + "defaultMessage": "Fine tune permissions for this user" + }, + "seconds": { + "defaultMessage": "{seconds} seconds" + }, + "set-password": { + "defaultMessage": "Set Password" + }, + "settings.title": { + "defaultMessage": "Settings" + }, + "setup.create": { + "defaultMessage": "Sign up" + }, + "setup.title": { + "defaultMessage": "Create your first Account" + }, + "ssl.title": { + "defaultMessage": "SSL" + }, + "status.failed": { + "defaultMessage": "Failed" + }, + "status.ok": { + "defaultMessage": "OK" + }, + "status.provided": { + "defaultMessage": "Provided" + }, + "status.ready": { + "defaultMessage": "Ready" + }, + "status.requesting": { + "defaultMessage": "Requesting" + }, + "tables.clear-all-filters": { + "defaultMessage": "Clear {count, plural, one {filter} other {# filters}}" + }, + "tables.no-items": { + "defaultMessage": "There are no items" + }, + "tables.no-items-with-filters": { + "defaultMessage": "There are no items matching {count, plural, one {this filter} other {these filters}}" + }, + "tables.pagination-counts": { + "defaultMessage": "Showing {start} to {end} of {total, plural, =0 {no items} one {# item} other {# items}}" + }, + "tables.pagination-next": { + "defaultMessage": "Next page" + }, + "tables.pagination-previous": { + "defaultMessage": "Previous page" + }, + "tables.pagination-select": { + "defaultMessage": "Select a page" + }, + "theme.to-dark": { + "defaultMessage": "Switch to dark theme" + }, + "theme.to-light": { + "defaultMessage": "Switch to light theme" + }, + "type.custom": { + "defaultMessage": "Custom" + }, + "type.dead": { + "defaultMessage": "404 Host" + }, + "type.dns": { + "defaultMessage": "DNS" + }, + "type.http": { + "defaultMessage": "HTTP" + }, + "type.mkcert": { + "defaultMessage": "MKCert" + }, + "type.proxy": { + "defaultMessage": "Proxy Host" + }, + "type.redirect": { + "defaultMessage": "Redirection" + }, + "type.stream": { + "defaultMessage": "Stream" + }, + "type.upstream": { + "defaultMessage": "Upstream" + }, + "unhealthy.body": { + "defaultMessage": "We'll continue to check the health and hope to be back up and running soon!" + }, + "unhealthy.title": { + "defaultMessage": "Nginx Proxy Manager is unhealthy" + }, + "upstream.create": { + "defaultMessage": "Create Upstream" + }, + "upstreams.title": { + "defaultMessage": "Upstreams" + }, + "user.capabilities": { + "defaultMessage": "Capabilities" + }, + "user.create": { + "defaultMessage": "Create User" + }, + "user.disabled": { + "defaultMessage": "User is Disabled" + }, + "user.edit": { + "defaultMessage": "Edit User" + }, + "user.email": { + "defaultMessage": "Email" + }, + "user.name": { + "defaultMessage": "Name" + }, + "user.password": { + "defaultMessage": "Password" + }, + "users.title": { + "defaultMessage": "Users" + }, + "view-only": { + "defaultMessage": "View Only" + }, + "wildcards-not-permitted": { + "defaultMessage": "Wildcards not permitted for this type" + }, + "wildcards-not-supported": { + "defaultMessage": "Wildcards not supported for this CA" + } +} diff --git a/frontend/src/locale/src/fa.json b/frontend/src/locale/src/fa.json new file mode 100644 index 000000000..8f546c109 --- /dev/null +++ b/frontend/src/locale/src/fa.json @@ -0,0 +1,464 @@ +{ + "access-list.create": { + "defaultMessage": "Create Access List" + }, + "access-lists.title": { + "defaultMessage": "دسترسی به لیست ها" + }, + "action.delete": { + "defaultMessage": "Delete" + }, + "action.download": { + "defaultMessage": "Download" + }, + "action.edit": { + "defaultMessage": "ویرایش کنید" + }, + "action.nginx-config": { + "defaultMessage": "View Nginx Config" + }, + "action.renew": { + "defaultMessage": "Renew" + }, + "action.set-password": { + "defaultMessage": "قراردادن رمز عبور" + }, + "audit-log.title": { + "defaultMessage": "گزارش حسابرسی" + }, + "brand.name": { + "defaultMessage": "Nginx Proxy Manager" + }, + "capability-count": { + "defaultMessage": "{count} مورد" + }, + "capability.full-admin": { + "defaultMessage": "ادمین کامل" + }, + "capability.hosts.view": { + "defaultMessage": "مشاهده میزبان ها" + }, + "capability.system": { + "defaultMessage": "سیستم" + }, + "capability.users.manage": { + "defaultMessage": "مدیریت کاربران" + }, + "certificate-authorities.title": { + "defaultMessage": "مقامات صدور گواهینامه" + }, + "certificate-authority": { + "defaultMessage": "Certificate Authority" + }, + "certificate-authority.acmesh-server": { + "defaultMessage": "سرور ACME" + }, + "certificate-authority.ca-bundle": { + "defaultMessage": "بسته گواهی CA" + }, + "certificate-authority.create": { + "defaultMessage": "ایجاد مرجع صدور گواهینامه" + }, + "certificate-authority.edit": { + "defaultMessage": "ویرایش مرجع صدور گواهی" + }, + "certificate-authority.has-wildcard-support": { + "defaultMessage": "دارای پشتیبانی Wildcard" + }, + "certificate-authority.max-domains": { + "defaultMessage": "حداکثر دامنه در هر گواهی" + }, + "certificate.create": { + "defaultMessage": "ایجاد گواهی" + }, + "certificate.deleted": { + "defaultMessage": "Certificate has been deleted" + }, + "certificate.renewal-requested": { + "defaultMessage": "Renewal has been queued" + }, + "certificates.title": { + "defaultMessage": "گواهینامه ها" + }, + "change-password": { + "defaultMessage": "رمز عبور را تغییر دهید" + }, + "column.acmesh-name": { + "defaultMessage": "پلاگین Acme.sh" + }, + "column.description": { + "defaultMessage": "شرح" + }, + "column.dns-sleep": { + "defaultMessage": "DNS صبر کنید" + }, + "column.domain-names": { + "defaultMessage": "دامنه ها" + }, + "column.id": { + "defaultMessage": "شناسه" + }, + "column.max-domains": { + "defaultMessage": "دامنه در هر گواهی" + }, + "column.servers": { + "defaultMessage": "Servers" + }, + "column.status": { + "defaultMessage": "وضعیت" + }, + "column.type": { + "defaultMessage": "تایپ کنید" + }, + "column.validation-type": { + "defaultMessage": "اعتبار سنجی" + }, + "column.wildcard-support": { + "defaultMessage": "پشتیبانی Wildcard" + }, + "create-access-list-title": { + "defaultMessage": "Create Access List" + }, + "create-certificate-title": { + "defaultMessage": "هیچ گواهی وجود ندارد" + }, + "create-dns-provider": { + "defaultMessage": "ارائه دهنده DNS ایجاد کنید" + }, + "create-dns-provider-title": { + "defaultMessage": "هیچ ارائه دهنده DNS وجود ندارد" + }, + "create-hint": { + "defaultMessage": "چرا یکی را ایجاد نمی کنید؟" + }, + "create-nginx-template": { + "defaultMessage": "قالب هاست ایجاد کنید" + }, + "create-host-title": { + "defaultMessage": "هیچ هاست پروکسی وجود ندارد" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, + "dashboard.title": { + "defaultMessage": "داشبورد" + }, + "disabled": { + "defaultMessage": "معلول" + }, + "dns-provider": { + "defaultMessage": "DNS Provider" + }, + "dns-provider.acmesh-name": { + "defaultMessage": "نام Acme.sh" + }, + "dns-provider.create": { + "defaultMessage": "ارائه دهنده DNS ایجاد کنید" + }, + "dns-providers.empty": { + "defaultMessage": "No DNS Providers - Create one first" + }, + "domain_names": { + "defaultMessage": "Domain Names" + }, + "domain_names.max": { + "defaultMessage": "{count} domain names maximum" + }, + "dns-providers.title": { + "defaultMessage": "ارائه دهندگان DNS" + }, + "error": { + "defaultMessage": "خطا" + }, + "error.ca-bundle-does-not-exist": { + "defaultMessage": "فایل در سرور وجود ندارد" + }, + "error.cannot-save-system-users": { + "defaultMessage": "شما نمی توانید کاربران سیستم را تغییر دهید" + }, + "error.current-password-invalid": { + "defaultMessage": "رمز عبور فعلی نامعتبر است" + }, + "error.database-unavailable": { + "defaultMessage": "پایگاه داده در دسترس نیست" + }, + "error.email-already-exists": { + "defaultMessage": "کاربری از قبل با این آدرس ایمیل وجود دارد" + }, + "error.invalid-auth-type": { + "defaultMessage": "Invalid authentication type" + }, + "error.invalid-login-credentials": { + "defaultMessage": "اعتبار ورود نامعتبر است" + }, + "error.provider_not_found": { + "defaultMessage": "Provider not found" + }, + "error.request-failed-validation": { + "defaultMessage": "اعتبار سنجی پشتیبان ناموفق بود" + }, + "error.user-disabled": { + "defaultMessage": "اکانت غیرفعال است" + }, + "filter.apply": { + "defaultMessage": "درخواست دادن" + }, + "filter.clear": { + "defaultMessage": "پاک کردن" + }, + "filter.contains": { + "defaultMessage": "حاوی" + }, + "filter.ends": { + "defaultMessage": "به پایان می رسد با" + }, + "filter.exactly": { + "defaultMessage": "دقیقا" + }, + "filter.placeholder": { + "defaultMessage": "عبارت جستجو را وارد کنید" + }, + "filter.starts": { + "defaultMessage": "شروع با" + }, + "footer.changelog": { + "defaultMessage": "ورود به سیستم را تغییر دهید" + }, + "footer.copyright": { + "defaultMessage": "حق چاپ © حق چاپ © {year} jc21.com" + }, + "footer.github": { + "defaultMessage": "Github" + }, + "footer.userguide": { + "defaultMessage": "راهنمای کاربر" + }, + "form.cancel": { + "defaultMessage": "لغو کنید" + }, + "form.invalid-email": { + "defaultMessage": "آدرس ایمیل نامعتبر" + }, + "form.max-int": { + "defaultMessage": "حداکثر {count} است" + }, + "form.max-length": { + "defaultMessage": "حداکثر طول {count, plural, one {# character} other {# characters}} کاراکتر است" + }, + "form.min-int": { + "defaultMessage": "حداقل {count} است" + }, + "form.min-length": { + "defaultMessage": "حداقل طول {count, plural, one {# character} other {# characters}} کاراکتر است" + }, + "form.required": { + "defaultMessage": "این مورد نیاز است" + }, + "form.save": { + "defaultMessage": "صرفه جویی" + }, + "full-access": { + "defaultMessage": "دسترسی کامل" + }, + "full-access.description": { + "defaultMessage": "دسترسی به تمام قابلیت ها" + }, + "general-settings.title": { + "defaultMessage": "تنظیمات عمومی" + }, + "nginx-templates.title": { + "defaultMessage": "قالب های میزبان" + }, + "host.create": { + "defaultMessage": "هاست ایجاد کنید" + }, + "hosts.title": { + "defaultMessage": "میزبان" + }, + "http-https": { + "defaultMessage": "HTTP/HTTPS" + }, + "http-only": { + "defaultMessage": "فقط HTTP" + }, + "https-only": { + "defaultMessage": "فقط HTTPS" + }, + "lets-go": { + "defaultMessage": "بیا بریم" + }, + "is-ecc": { + "defaultMessage": "ECC Certificate" + }, + "login.login": { + "defaultMessage": "ورود" + }, + "name": { + "defaultMessage": "نام" + }, + "navigation.close": { + "defaultMessage": "بستن ناوبری" + }, + "navigation.open": { + "defaultMessage": "ناوبری را باز کنید" + }, + "nginx-config": { + "defaultMessage": "Nginx Config" + }, + "no-access": { + "defaultMessage": "هیچ دسترسی" + }, + "password.confirm": { + "defaultMessage": "رمز عبور جدید را تأیید کنید" + }, + "password.current": { + "defaultMessage": "رمز عبور فعلی" + }, + "password.new": { + "defaultMessage": "رمز عبور جدید" + }, + "permissions.title": { + "defaultMessage": "مجوزها" + }, + "profile.logout": { + "defaultMessage": "خروج" + }, + "profile.title": { + "defaultMessage": "تنظیمات نمایه" + }, + "restricted-access": { + "defaultMessage": "دسترسی محدود" + }, + "restricted-access.description": { + "defaultMessage": "مجوزهای تنظیم دقیق برای این کاربر" + }, + "seconds": { + "defaultMessage": "{seconds} ثانیه" + }, + "set-password": { + "defaultMessage": "قراردادن رمز عبور" + }, + "settings.title": { + "defaultMessage": "تنظیمات" + }, + "setup.create": { + "defaultMessage": "ثبت نام" + }, + "setup.title": { + "defaultMessage": "اولین حساب خود را ایجاد کنید" + }, + "ssl.title": { + "defaultMessage": "SSL" + }, + "status.failed": { + "defaultMessage": "Failed" + }, + "status.ok": { + "defaultMessage": "OK" + }, + "status.ready": { + "defaultMessage": "آماده" + }, + "status.requesting": { + "defaultMessage": "Requesting" + }, + "tables.clear-all-filters": { + "defaultMessage": "{count, plural, one {فیلتر را پاک کنید} other {# فیلتر را پاک کنید}}" + }, + "tables.no-items": { + "defaultMessage": "هیچ آیتمی وجود ندارد" + }, + "tables.no-items-with-filters": { + "defaultMessage": "{count, plural, one {هیچ موردی مطابق با این فیلتر وجود ندارد} other {هیچ موردی مطابق با این فیلترها وجود ندارد}}" + }, + "tables.pagination-counts": { + "defaultMessage": "نمایش {start} تا {end} مورد از {total} مورد" + }, + "tables.pagination-next": { + "defaultMessage": "صفحه بعد" + }, + "tables.pagination-previous": { + "defaultMessage": "صفحه قبلی" + }, + "tables.pagination-select": { + "defaultMessage": "یک صفحه را انتخاب کنید" + }, + "theme.to-dark": { + "defaultMessage": "به طرح زمینه تیره بروید" + }, + "theme.to-light": { + "defaultMessage": "به طرح زمینه روشن تغییر دهید" + }, + "type.custom": { + "defaultMessage": "Custom" + }, + "type.dead": { + "defaultMessage": "404 Host" + }, + "type.dns": { + "defaultMessage": "DNS" + }, + "type.http": { + "defaultMessage": "HTTP" + }, + "type.mkcert": { + "defaultMessage": "MKCert" + }, + "type.proxy": { + "defaultMessage": "Proxy Host" + }, + "type.redirect": { + "defaultMessage": "Redirection" + }, + "type.stream": { + "defaultMessage": "Stream" + }, + "type.upstream": { + "defaultMessage": "Upstream" + }, + "unhealthy.body": { + "defaultMessage": "ما همچنان به بررسی وضعیت سلامتی خود ادامه خواهیم داد و امیدواریم به زودی دوباره راه اندازی شده و کار کنیم!" + }, + "unhealthy.title": { + "defaultMessage": "Nginx Proxy Manager ناسالم است" + }, + "upstream.create": { + "defaultMessage": "Create Upstream" + }, + "upstreams.title": { + "defaultMessage": "Upstreams" + }, + "user.capabilities": { + "defaultMessage": "توانایی ها" + }, + "user.create": { + "defaultMessage": "کاربر ایجاد کنید" + }, + "user.disabled": { + "defaultMessage": "کاربر غیرفعال است" + }, + "user.edit": { + "defaultMessage": "ویرایش کاربر" + }, + "user.email": { + "defaultMessage": "پست الکترونیک" + }, + "user.name": { + "defaultMessage": "نام" + }, + "user.password": { + "defaultMessage": "کلمه عبور" + }, + "users.title": { + "defaultMessage": "کاربران" + }, + "view-only": { + "defaultMessage": "مشاهده فقط" + }, + "wildcards-not-permitted": { + "defaultMessage": "Wildcards not permitted for this type" + }, + "wildcards-not-supported": { + "defaultMessage": "Wildcards not supported for this CA" + } +} diff --git a/frontend/src/locale/src/lang-list.json b/frontend/src/locale/src/lang-list.json new file mode 100644 index 000000000..cdd95d05e --- /dev/null +++ b/frontend/src/locale/src/lang-list.json @@ -0,0 +1,11 @@ +{ + "locale-de-DE": { + "defaultMessage": "Deutsch" + }, + "locale-en-US": { + "defaultMessage": "English" + }, + "locale-fa-IR": { + "defaultMessage": "فارسی" + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 000000000..baadc80d8 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +import { ColorModeScript } from "@chakra-ui/react"; +import ReactDOM from "react-dom/client"; + +import App from "./App"; +import "./index.scss"; +import customTheme from "./theme/customTheme"; + +declare global { + interface Function { + Item: React.FC; + Link: React.FC; + Header: React.FC; + Main: React.FC; + Options: React.FC; + SubTitle: React.FC; + Title: React.FC; + } +} + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + , +); diff --git a/frontend/src/modals/AccessListCreateModal.tsx b/frontend/src/modals/AccessListCreateModal.tsx new file mode 100644 index 000000000..83c228bff --- /dev/null +++ b/frontend/src/modals/AccessListCreateModal.tsx @@ -0,0 +1,223 @@ +// TODO +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface AccessListCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function AccessListCreateModal({ + isOpen, + onClose, +}: AccessListCreateModalProps) { + const toast = useToast(); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.create" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { AccessListCreateModal }; diff --git a/frontend/src/modals/CertificateAuthorityCreateModal.tsx b/frontend/src/modals/CertificateAuthorityCreateModal.tsx new file mode 100644 index 000000000..80332aebf --- /dev/null +++ b/frontend/src/modals/CertificateAuthorityCreateModal.tsx @@ -0,0 +1,222 @@ +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface CertificateAuthorityCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function CertificateAuthorityCreateModal({ + isOpen, + onClose, +}: CertificateAuthorityCreateModalProps) { + const toast = useToast(); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.create" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { CertificateAuthorityCreateModal }; diff --git a/frontend/src/modals/CertificateAuthorityEditModal.tsx b/frontend/src/modals/CertificateAuthorityEditModal.tsx new file mode 100644 index 000000000..b39471bb9 --- /dev/null +++ b/frontend/src/modals/CertificateAuthorityEditModal.tsx @@ -0,0 +1,241 @@ +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useCertificateAuthority, useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface CertificateAuthorityEditModalProps { + editId: number; + isOpen: boolean; + onClose: () => void; +} +function CertificateAuthorityEditModal({ + editId, + isOpen, + onClose, +}: CertificateAuthorityEditModalProps) { + const toast = useToast(); + const { status, data } = useCertificateAuthority(editId); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + { + onClose(); + }} + closeOnOverlayClick={false}> + + + {status === "pending" ? ( + // todo nicer +

loading

+ ) : ( + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.edit" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+ )} +
+
+ ); +} + +export { CertificateAuthorityEditModal }; diff --git a/frontend/src/modals/CertificateCreateModal/CertificateCreateModal.tsx b/frontend/src/modals/CertificateCreateModal/CertificateCreateModal.tsx new file mode 100644 index 000000000..59f4e0bd7 --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/CertificateCreateModal.tsx @@ -0,0 +1,147 @@ +import { + Button, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form } from "formik"; + +import { Certificate } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetCertificate } from "src/hooks"; +import { intl } from "src/locale"; + +import CustomForm from "./CustomForm"; +import DNSForm from "./DNSForm"; +import HTTPForm from "./HTTPForm"; +import MKCertForm from "./MKCertForm"; + +interface CertificateCreateModalProps { + isOpen: boolean; + onClose: () => void; + certType: string; +} +function CertificateCreateModal({ + isOpen, + onClose, + certType, +}: CertificateCreateModalProps) { + const toast = useToast(); + const { mutate: setCertificate } = useSetCertificate(); + + const onModalClose = () => { + onClose(); + }; + + const onSubmit = async ( + payload: Certificate, + { /*setErrors,*/ setSubmitting }: any, + ) => { + payload.type = certType; + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificate(payload, { + onError: (err: any) => { + showErr(err.message); + /* + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + }*/ + }, + onSuccess: () => onModalClose(), + onSettled: () => setSubmitting(false), + }); + }; + + const getInitialValues = (type: string): any => { + switch (type) { + case "http": + return { + certificateAuthorityId: 0, + name: "", + domainNames: [], + isEcc: false, + } as any; + case "dns": + return { + certificateAuthorityId: 0, + dnsProviderId: 0, + name: "", + domainNames: [], + isEcc: false, + } as any; + case "custom": + return { + name: "", + domainNames: [], + // isEcc: false, // todo, required? + // todo: add meta? + } as any; + case "mkcert": + return { + name: "", + domainNames: [], + // isEcc: false, // todo, supported? + // todo: add meta? + } as any; + } + }; + + return ( + + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate.create" })} + + + + + {certType === "http" ? : null} + {certType === "dns" ? : null} + {certType === "custom" ? : null} + {certType === "mkcert" ? : null} + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { CertificateCreateModal }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/CertificateAuthorityField.tsx b/frontend/src/modals/CertificateCreateModal/Common/CertificateAuthorityField.tsx new file mode 100644 index 000000000..16e9d62ff --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/CertificateAuthorityField.tsx @@ -0,0 +1,78 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + Select, +} from "@chakra-ui/react"; +import { Field, useFormikContext } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { useCertificateAuthorities } from "src/hooks"; +import { intl } from "src/locale"; + +const fieldName = "certificateAuthorityId"; + +interface CertificateAuthorityFieldProps { + onChange?: (maxDomains: number, isWildcardSupported: boolean) => any; +} +function CertificateAuthorityField({ + onChange, +}: CertificateAuthorityFieldProps) { + const { setFieldValue } = useFormikContext(); + const { data, isLoading } = useCertificateAuthorities(0, 999, [{ id: "id" }]); + + const handleOnChange = (e: any) => { + if (e.currentTarget.value) { + const id = parseInt(e.currentTarget.value, 10); + // This step enforces that the formik payload has a + // string number instead of a string as the value + // for this field + setFieldValue(fieldName, id); + if (onChange) { + // find items in list of data + const ca = data?.items.find((item) => item.id === id); + if (ca) { + onChange(ca.maxDomains, ca.isWildcardSupported); + } else { + onChange(0, false); + } + } + } + }; + + return ( + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority", + })} + + + {form.errors[fieldName]} + + )} + + ); +} + +export { CertificateAuthorityField }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/DNSProviderField.tsx b/frontend/src/modals/CertificateCreateModal/Common/DNSProviderField.tsx new file mode 100644 index 000000000..958bdd436 --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/DNSProviderField.tsx @@ -0,0 +1,74 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + Select, +} from "@chakra-ui/react"; +import { Field, useFormikContext } from "formik"; + +import { DNSProvider } from "src/api/npm"; +import { useDNSProviders } from "src/hooks"; +import { intl } from "src/locale"; + +const fieldName = "dnsProviderId"; + +function DNSProviderField() { + const { setFieldValue } = useFormikContext(); + const { data, isLoading } = useDNSProviders(0, 999); + + const handleOnChange = (e: any) => { + if (e.currentTarget.value) { + const id = parseInt(e.currentTarget.value, 10); + // This step enforces that the formik payload has a + // string number instead of a string as the value + // for this field + setFieldValue(fieldName, id); + } + }; + + return ( + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "dns-provider", + })} + + + + {!isLoading && !data?.total + ? intl.formatMessage({ + id: "dns-providers.empty", + }) + : form.errors[fieldName]} + + + )} + + ); +} + +export { DNSProviderField }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/DomainNamesField.tsx b/frontend/src/modals/CertificateCreateModal/Common/DomainNamesField.tsx new file mode 100644 index 000000000..4f55dd60b --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/DomainNamesField.tsx @@ -0,0 +1,123 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + FormHelperText, +} from "@chakra-ui/react"; +import { CreatableSelect, OptionBase } from "chakra-react-select"; +import { Field, useFormikContext } from "formik"; + +import { intl } from "src/locale"; + +interface SelectOption extends OptionBase { + label: string; + value: string; +} +interface DomainNamesFieldProps { + maxDomains?: number; + isWildcardPermitted?: boolean; + dnsProviderWildcardSupported?: boolean; + // onChange?: (i: string[]) => any; +} +function DomainNamesField({ + maxDomains, + isWildcardPermitted, + dnsProviderWildcardSupported, // onChange, +}: DomainNamesFieldProps) { + const { values, setFieldValue } = useFormikContext(); + + const getDomainCount = (v: string[] | undefined) => { + if (typeof v !== "undefined" && v?.length) { + return v.length; + } + return 0; + }; + + const isDomainValid = (d: string): boolean => { + const dom = d.trim().toLowerCase(); + const v: any = values; + + // Deny if the list of domains is hit + if (maxDomains && getDomainCount(v?.domainNames) >= maxDomains) { + return false; + } + + if (dom.length < 3) { + return false; + } + + // Prevent wildcards + if ( + (!isWildcardPermitted || !dnsProviderWildcardSupported) && + dom.indexOf("*") !== -1 + ) { + return false; + } + + // Prevent duplicate * in domain + if ((dom.match(/\*/g) || []).length > 1) { + return false; + } + + // Prevent some invalid characters + // @ , + if ((dom.match(/(@|,)/g) || []).length > 0) { + return false; + } + + // This will match *.com type domains, + return dom.match(/\*\.[^.]+$/m) === null; + }; + + const handleChange = (values: any) => { + const doms = values?.map((i: SelectOption) => { + return i.value; + }); + setFieldValue("domainNames", doms); + }; + + const helperTexts: string[] = []; + if (maxDomains) { + helperTexts.push( + intl.formatMessage({ id: "domain_names.max" }, { count: maxDomains }), + ); + } + if (!isWildcardPermitted) { + helperTexts.push(intl.formatMessage({ id: "wildcards-not-permitted" })); + } else if (!dnsProviderWildcardSupported) { + helperTexts.push(intl.formatMessage({ id: "wildcards-not-supported" })); + } + + return ( + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "domain_names", + })} + + + {helperTexts.length + ? helperTexts.map((i) => { + return {i}; + }) + : null} + {form.errors.domainNames} + + )} + + ); +} + +export { DomainNamesField }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/EccField.tsx b/frontend/src/modals/CertificateCreateModal/Common/EccField.tsx new file mode 100644 index 000000000..543cf8f08 --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/EccField.tsx @@ -0,0 +1,32 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + Switch, +} from "@chakra-ui/react"; +import { Field } from "formik"; + +import { intl } from "src/locale"; + +const fieldName = "isEcc"; + +function EccField() { + return ( + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "is-ecc", + })} + + + {form.errors[fieldName]} + + )} + + ); +} + +export { EccField }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/NameField.tsx b/frontend/src/modals/CertificateCreateModal/Common/NameField.tsx new file mode 100644 index 000000000..d9652a2dd --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/NameField.tsx @@ -0,0 +1,38 @@ +import { + FormControl, + FormErrorMessage, + FormLabel, + Input, +} from "@chakra-ui/react"; +import { Field } from "formik"; + +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +function NameField() { + return ( + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + {form.errors.name} + + )} + + ); +} + +export { NameField }; diff --git a/frontend/src/modals/CertificateCreateModal/Common/index.ts b/frontend/src/modals/CertificateCreateModal/Common/index.ts new file mode 100644 index 000000000..fde5c13cc --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/Common/index.ts @@ -0,0 +1,5 @@ +export * from "./CertificateAuthorityField"; +export * from "./DNSProviderField"; +export * from "./DomainNamesField"; +export * from "./EccField"; +export * from "./NameField"; diff --git a/frontend/src/modals/CertificateCreateModal/CustomForm.tsx b/frontend/src/modals/CertificateCreateModal/CustomForm.tsx new file mode 100644 index 000000000..75291eede --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/CustomForm.tsx @@ -0,0 +1,12 @@ +import { DomainNamesField, NameField } from "./Common"; + +function CustomForm() { + return ( + <> + + + + ); +} + +export default CustomForm; diff --git a/frontend/src/modals/CertificateCreateModal/DNSForm.tsx b/frontend/src/modals/CertificateCreateModal/DNSForm.tsx new file mode 100644 index 000000000..2f55b4dc1 --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/DNSForm.tsx @@ -0,0 +1,35 @@ +import { useState } from "react"; + +import { + CertificateAuthorityField, + DNSProviderField, + DomainNamesField, + EccField, + NameField, +} from "./Common"; + +function DNSForm() { + const [maxDomains, setMaxDomains] = useState(0); + const [isWildcardSupported, setIsWildcardSupported] = useState(false); + + const handleCAChange = (maxD: number, wildcards: boolean) => { + setMaxDomains(maxD); + setIsWildcardSupported(wildcards); + }; + + return ( + <> + + + + + + + ); +} + +export default DNSForm; diff --git a/frontend/src/modals/CertificateCreateModal/HTTPForm.tsx b/frontend/src/modals/CertificateCreateModal/HTTPForm.tsx new file mode 100644 index 000000000..f2cb118ca --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/HTTPForm.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; + +import { + CertificateAuthorityField, + DomainNamesField, + EccField, + NameField, +} from "./Common"; + +function HTTPForm() { + const [maxDomains, setMaxDomains] = useState(0); + const [isWildcardSupported, setIsWildcardSupported] = useState(false); + + const handleCAChange = (maxD: number, wildcards: boolean) => { + setMaxDomains(maxD); + setIsWildcardSupported(wildcards); + }; + + return ( + <> + + + + + + ); +} + +export default HTTPForm; diff --git a/frontend/src/modals/CertificateCreateModal/MKCertForm.tsx b/frontend/src/modals/CertificateCreateModal/MKCertForm.tsx new file mode 100644 index 000000000..513631e8a --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/MKCertForm.tsx @@ -0,0 +1,12 @@ +import { DomainNamesField, NameField } from "./Common"; + +function MKCertForm() { + return ( + <> + + + + ); +} + +export default MKCertForm; diff --git a/frontend/src/modals/CertificateCreateModal/index.ts b/frontend/src/modals/CertificateCreateModal/index.ts new file mode 100644 index 000000000..948309057 --- /dev/null +++ b/frontend/src/modals/CertificateCreateModal/index.ts @@ -0,0 +1 @@ +export * from "./CertificateCreateModal"; diff --git a/frontend/src/modals/CertificateEditModal.tsx b/frontend/src/modals/CertificateEditModal.tsx new file mode 100644 index 000000000..412a3a347 --- /dev/null +++ b/frontend/src/modals/CertificateEditModal.tsx @@ -0,0 +1,241 @@ +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useCertificateAuthority, useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface CertificateEditModalProps { + editId: number; + isOpen: boolean; + onClose: () => void; +} +function CertificateEditModal({ + editId, + isOpen, + onClose, +}: CertificateEditModalProps) { + const toast = useToast(); + const { status, data } = useCertificateAuthority(editId); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + { + onClose(); + }} + closeOnOverlayClick={false}> + + + {status === "pending" ? ( + // todo nicer +

loading

+ ) : ( + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.edit" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+ )} +
+
+ ); +} + +export { CertificateEditModal }; diff --git a/frontend/src/modals/ChangePasswordModal.tsx b/frontend/src/modals/ChangePasswordModal.tsx new file mode 100644 index 000000000..1bdb1a13f --- /dev/null +++ b/frontend/src/modals/ChangePasswordModal.tsx @@ -0,0 +1,181 @@ +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; + +import { setAuth } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +interface ChangePasswordModalProps { + isOpen: boolean; + onClose: () => void; +} +function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) { + const toast = useToast(); + + const onSubmit = async (payload: any, { setSubmitting, setErrors }: any) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + try { + await setAuth("me", { + type: "local", + secret: payload.password, + currentSecret: payload.current, + }); + onClose(); + } catch (err: any) { + if (err.message === "current-password-invalid") { + setErrors({ + current: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + } + setSubmitting(false); + }; + + return ( + + + + { + const errors = {} as any; + if (values.password !== values.password2) { + errors.password2 = "New passwords do not match"; + } + return errors; + }}> + {({ isSubmitting }: any) => ( +
+ + {intl.formatMessage({ id: "change-password" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "password.current" })} + + + + {form.errors.current} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "password.new" })} + + + + {form.errors.password} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "password.confirm" })} + + + + {form.errors.password2} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { ChangePasswordModal }; diff --git a/frontend/src/modals/DNSProviderCreateModal.tsx b/frontend/src/modals/DNSProviderCreateModal.tsx new file mode 100644 index 000000000..417ce57e6 --- /dev/null +++ b/frontend/src/modals/DNSProviderCreateModal.tsx @@ -0,0 +1,314 @@ +import { useEffect, useState } from "react"; + +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Select, + Stack, + useToast, +} from "@chakra-ui/react"; +import Ajv, { Schema } from "ajv"; +import { Formik, Form, Field, getIn } from "formik"; + +import { + DNSProvider, + DNSProvidersAcmesh, + DNSProvidersAcmeshProperty, +} from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetDNSProvider, useDNSProvidersAcmesh } from "src/hooks"; +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +interface DNSProviderCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function DNSProviderCreateModal({ + isOpen, + onClose, +}: DNSProviderCreateModalProps) { + const toast = useToast(); + const { mutate: setDNSProvider } = useSetDNSProvider(); + const { isLoading: acmeshIsLoading, data: acmeshDataResp } = + useDNSProvidersAcmesh(); + + const [acmeshData, setAcmeshData] = useState([] as DNSProvidersAcmesh[]); + const [acmeshItem, setAcmeshItem] = useState(""); + + useEffect(() => { + setAcmeshData(acmeshDataResp || []); + }, [acmeshDataResp]); + + const onModalClose = () => { + setAcmeshItem(""); + onClose(); + }; + + const getAcmeshItem = (name: string): DNSProvidersAcmesh | undefined => { + return acmeshData.find((item) => item.title === name); + }; + + const fullItem = getAcmeshItem(acmeshItem); + const itemProperties = fullItem?.properties; + + const onSubmit = async ( + payload: DNSProvider, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + const ajv = new Ajv({ strictSchema: false }); + try { + const valid = ajv.validate(fullItem as Schema, payload.meta); + if (!valid) { + const errs: any = {}; + ajv.errors?.forEach((e: any) => { + errs["meta"] = { + [e.instancePath.substring(1)]: e.message, + }; + }); + setErrors(errs); + setSubmitting(false); + } else { + // Json schema is happy + setDNSProvider(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onModalClose(), + onSettled: () => setSubmitting(false), + }); + } + } catch (err: any) { + showErr(err); + setSubmitting(false); + } + }; + + const renderInputType = ( + field: any, + fieldName: string, + f: DNSProvidersAcmeshProperty, + value: any, + ) => { + if (["bool", "boolean"].indexOf(f.type) !== -1) { + return ( + + {f.title} + + ); + } + + let type = "text"; + const props: any = {}; + + if (f.type === "string") { + props.minLength = f.minLength || null; + props.maxLength = f.maxLength || null; + props.pattern = f.pattern || null; + } + if (f.type === "integer") { + type = "number"; + props.min = f.minimum || null; + props.max = f.maximum || null; + } + if (f.isSecret) { + type = "password"; + } + + return ( + + ); + }; + + return ( + + + + {acmeshIsLoading ? ( + "loading" + ) : ( + + {({ isSubmitting, handleChange, values }) => ( +
+ + {intl.formatMessage({ id: "dns-provider.create" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "dns-provider.acmesh-name", + })} + + + + {form.errors.acmeshName} + + + )} + + {acmeshItem !== "" ? ( + <> + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + + {form.errors.name} + + + )} + + {itemProperties + ? Object.keys(itemProperties).map((fieldName) => { + const f = itemProperties[fieldName]; + const name = `meta[${fieldName}]`; + return ( + + {({ field, form }: any) => ( + + {f.type !== "bool" ? ( + + {intl.formatMessage({ + id: `acmesh-property.${f.title}`, + })} + + ) : null} + {renderInputType( + field, + fieldName, + f, + values.meta[f.title], + )} + + {form.errors?.meta?.[fieldName]} + + + )} + + ); + }) + : null} + + ) : null} + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+ )} +
+
+ ); +} + +export { DNSProviderCreateModal }; diff --git a/frontend/src/modals/HostCreateModal.tsx b/frontend/src/modals/HostCreateModal.tsx new file mode 100644 index 000000000..07f511ac7 --- /dev/null +++ b/frontend/src/modals/HostCreateModal.tsx @@ -0,0 +1,219 @@ +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface HostCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function HostCreateModal({ isOpen, onClose }: HostCreateModalProps) { + const toast = useToast(); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "host.create" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { HostCreateModal }; diff --git a/frontend/src/modals/ProfileModal.tsx b/frontend/src/modals/ProfileModal.tsx new file mode 100644 index 000000000..4b164b8af --- /dev/null +++ b/frontend/src/modals/ProfileModal.tsx @@ -0,0 +1,132 @@ +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; + +import { PrettyButton } from "src/components"; +import { useSetUser, useUser } from "src/hooks"; +import { intl } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; + +interface ProfileModalProps { + isOpen: boolean; + onClose: () => void; +} +function ProfileModal({ isOpen, onClose }: ProfileModalProps) { + const toast = useToast(); + const user = useUser("me"); + const { mutate: setUser } = useSetUser(); + + const onSubmit = (payload: any, { setSubmitting }: any) => { + payload.id = "me"; + + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setUser(payload, { + onError: (err: any) => showErr(err.message), + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + + + + + {({ isSubmitting, values }: any) => ( +
+ + {intl.formatMessage({ id: "profile.title" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.name" })} + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.email" })} + + + {form.errors.email} + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { ProfileModal }; diff --git a/frontend/src/modals/SetPasswordModal.tsx b/frontend/src/modals/SetPasswordModal.tsx new file mode 100644 index 000000000..76c037a61 --- /dev/null +++ b/frontend/src/modals/SetPasswordModal.tsx @@ -0,0 +1,153 @@ +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; + +import { setAuth } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { validateString } from "src/modules/Validations"; + +interface SetPasswordModalProps { + userId: number; + isOpen: boolean; + onClose: () => void; +} +function SetPasswordModal({ userId, isOpen, onClose }: SetPasswordModalProps) { + const toast = useToast(); + + const onSubmit = async (payload: any, { setSubmitting, setErrors }: any) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + try { + await setAuth(userId, { + type: "local", + secret: payload.password, + }); + onClose(); + } catch (err: any) { + if (err.message === "current-password-invalid") { + setErrors({ + current: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + } + setSubmitting(false); + }; + + return ( + + + + { + const errors = {} as any; + if (values.password !== values.password2) { + errors.password2 = "New passwords do not match"; + } + return errors; + }}> + {({ isSubmitting }: any) => ( +
+ + {intl.formatMessage({ id: "set-password" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "password.new" })} + + + + {form.errors.password} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "password.confirm" })} + + + + {form.errors.password2} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { SetPasswordModal }; diff --git a/frontend/src/modals/UpstreamCreateModal.tsx b/frontend/src/modals/UpstreamCreateModal.tsx new file mode 100644 index 000000000..804fb0232 --- /dev/null +++ b/frontend/src/modals/UpstreamCreateModal.tsx @@ -0,0 +1,220 @@ +// TODO +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface UpstreamCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function UpstreamCreateModal({ isOpen, onClose }: UpstreamCreateModalProps) { + const toast = useToast(); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.create" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + {form.errors.name} + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { UpstreamCreateModal }; diff --git a/frontend/src/modals/UpstreamEditModal.tsx b/frontend/src/modals/UpstreamEditModal.tsx new file mode 100644 index 000000000..bb014c1c9 --- /dev/null +++ b/frontend/src/modals/UpstreamEditModal.tsx @@ -0,0 +1,242 @@ +// TODO +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + ModalFooter, + Stack, + useToast, +} from "@chakra-ui/react"; +import { Formik, Form, Field } from "formik"; + +import { CertificateAuthority } from "src/api/npm"; +import { PrettyButton } from "src/components"; +import { useCertificateAuthority, useSetCertificateAuthority } from "src/hooks"; +import { intl } from "src/locale"; +import { validateNumber, validateString } from "src/modules/Validations"; + +interface UpstreamEditModalProps { + editId: number; + isOpen: boolean; + onClose: () => void; +} +function UpstreamEditModal({ + editId, + isOpen, + onClose, +}: UpstreamEditModalProps) { + const toast = useToast(); + const { status, data } = useCertificateAuthority(editId); + const { mutate: setCertificateAuthority } = useSetCertificateAuthority(); + + const onSubmit = async ( + payload: CertificateAuthority, + { setErrors, setSubmitting }: any, + ) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setCertificateAuthority(payload, { + onError: (err: any) => { + if (err.message === "ca-bundle-does-not-exist") { + setErrors({ + caBundle: intl.formatMessage({ + id: `error.${err.message}`, + }), + }); + } else { + showErr(err.message); + } + }, + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + return ( + { + onClose(); + }} + closeOnOverlayClick={false}> + + + {status === "pending" ? ( + // todo nicer +

loading

+ ) : ( + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "certificate-authority.edit" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "name", + })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.acmesh-server", + })} + + + + {form.errors.acmeshServer} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.ca-bundle", + })} + + + + {form.errors.caBundle} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.max-domains", + })} + + + + {form.errors.maxDomains} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "certificate-authority.has-wildcard-support", + })} + + + {form.errors.isWildcardSupported} + + + )} + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+ )} +
+
+ ); +} + +export { UpstreamEditModal }; diff --git a/frontend/src/modals/UpstreamNginxConfigModal.tsx b/frontend/src/modals/UpstreamNginxConfigModal.tsx new file mode 100644 index 000000000..4c16d03a8 --- /dev/null +++ b/frontend/src/modals/UpstreamNginxConfigModal.tsx @@ -0,0 +1,63 @@ +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, +} from "@chakra-ui/react"; +import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; +import sh from "react-syntax-highlighter/dist/esm/languages/hljs/bash"; +import nord from "react-syntax-highlighter/dist/esm/styles/hljs/nord"; + +import { useUpstreamNginxConfig } from "src/hooks"; +import { intl } from "src/locale"; + +interface UpstreamNginxConfigModalProps { + upstreamId: number; + isOpen: boolean; + onClose: () => void; +} +function UpstreamNginxConfigModal({ + isOpen, + onClose, + upstreamId, +}: UpstreamNginxConfigModalProps) { + const { isLoading, data } = useUpstreamNginxConfig(upstreamId); + SyntaxHighlighter.registerLanguage("bash", sh); + + return ( + + + + {isLoading ? ( + "loading" + ) : ( + <> + + {intl.formatMessage({ id: "nginx-config" })} + + + + + {data || ""} + + + + )} + + + ); +} + +export { UpstreamNginxConfigModal }; diff --git a/frontend/src/modals/UserCreateModal.tsx b/frontend/src/modals/UserCreateModal.tsx new file mode 100644 index 000000000..c68527139 --- /dev/null +++ b/frontend/src/modals/UserCreateModal.tsx @@ -0,0 +1,249 @@ +import { useState } from "react"; + +import { + Button, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + useToast, +} from "@chakra-ui/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Field, Form, Formik } from "formik"; + +import { createUser } from "src/api/npm"; +import { + AdminPermissionSelector, + PermissionSelector, + PrettyButton, +} from "src/components"; +import { intl } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; + +interface Payload { + name: string; + email: string; + password: string; +} + +interface UserCreateModalProps { + isOpen: boolean; + onClose: () => void; +} +function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) { + const toast = useToast(); + const queryClient = useQueryClient(); + const [capabilities, setCapabilities] = useState(["full-admin"]); + const [capabilityOption, setCapabilityOption] = useState("admin"); + + const onSubmit = async (values: Payload, { setSubmitting }: any) => { + const { ...payload } = { + ...values, + ...{ + isDisabled: false, + auth: { + type: "local", + secret: values.password, + }, + capabilities, + }, + }; + + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + try { + const response = await createUser(payload); + if (response && typeof response.id !== "undefined" && response.id) { + // ok + queryClient.invalidateQueries({ queryKey: ["users"] }); + onClose(); + resetForm(); + } else { + showErr("cannot_create_user"); + } + } catch (err: any) { + showErr(err.message); + } + setSubmitting(false); + }; + + const resetForm = () => { + setCapabilityOption("admin"); + setCapabilities(["full-admin"]); + }; + + return ( + { + resetForm(); + onClose(); + }} + closeOnOverlayClick={false}> + + + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "user.create" })} + + + + + + {intl.formatMessage({ id: "profile.title" })} + {intl.formatMessage({ id: "permissions.title" })} + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.name" })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.email" })} + + + + {form.errors.email} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.password" })} + + + + {form.errors.password} + + + )} + + + + + { + setCapabilityOption("admin"); + setCapabilities(["full-admin"]); + }} + /> + { + if (capabilityOption === "admin") { + setCapabilities([]); + } + setCapabilityOption("restricted"); + }} + onChange={setCapabilities} + capabilities={capabilities} + selected={capabilityOption === "restricted"} + /> + + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+
+
+ ); +} + +export { UserCreateModal }; diff --git a/frontend/src/modals/UserEditModal.tsx b/frontend/src/modals/UserEditModal.tsx new file mode 100644 index 000000000..2813edbe5 --- /dev/null +++ b/frontend/src/modals/UserEditModal.tsx @@ -0,0 +1,247 @@ +import { useEffect, useState } from "react"; + +import { + Button, + Checkbox, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + useToast, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; + +import { + AdminPermissionSelector, + PermissionSelector, + PrettyButton, +} from "src/components"; +import { useSetUser, useUser } from "src/hooks"; +import { intl } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; + +interface UserEditModalProps { + userId: number; + isOpen: boolean; + onClose: () => void; +} +function UserEditModal({ userId, isOpen, onClose }: UserEditModalProps) { + const toast = useToast(); + const { status, data } = useUser(userId); + const { mutate: setUser } = useSetUser(); + + const [capabilities, setCapabilities] = useState(data?.capabilities || []); + const [capabilityOption, setCapabilityOption] = useState( + data?.capabilities?.indexOf("full-admin") === -1 ? "restricted" : "admin", + ); + + useEffect(() => { + setCapabilities(data?.capabilities || []); + setCapabilityOption( + data?.capabilities?.indexOf("full-admin") === -1 ? "restricted" : "admin", + ); + }, [data]); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + const { ...payload } = { + id: userId, + ...values, + ...{ + capabilities, + }, + }; + + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + setUser(payload, { + onError: (err: any) => showErr(err.message), + onSuccess: () => onClose(), + onSettled: () => setSubmitting(false), + }); + }; + + const resetForm = () => { + setCapabilityOption("admin"); + setCapabilities(["full-admin"]); + }; + + return ( + { + resetForm(); + onClose(); + }} + closeOnOverlayClick={false}> + + + {status === "pending" ? ( + // todo nicer +

loading

+ ) : ( + + {({ isSubmitting }) => ( +
+ + {intl.formatMessage({ id: "user.edit" })} + + + + + + {intl.formatMessage({ id: "profile.title" })} + + {intl.formatMessage({ id: "permissions.title" })} + + + + + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.name" })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.email" })} + + + + {form.errors.email} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "user.disabled", + })} + + + {form.errors.isDisabled} + + + )} + + + + + { + setCapabilityOption("admin"); + setCapabilities(["full-admin"]); + }} + /> + { + if (capabilityOption === "admin") { + setCapabilities([]); + } + setCapabilityOption("restricted"); + }} + onChange={setCapabilities} + capabilities={capabilities} + selected={capabilityOption === "restricted"} + /> + + + + + + + {intl.formatMessage({ id: "form.save" })} + + + + + )} +
+ )} +
+
+ ); +} + +export { UserEditModal }; diff --git a/frontend/src/modals/index.ts b/frontend/src/modals/index.ts new file mode 100644 index 000000000..dbfecf831 --- /dev/null +++ b/frontend/src/modals/index.ts @@ -0,0 +1,16 @@ +export * from "./AccessListCreateModal"; +export * from "./CertificateAuthorityCreateModal"; +export * from "./CertificateAuthorityEditModal"; +export * from "./CertificateCreateModal"; +export * from "./CertificateEditModal"; +export * from "./ChangePasswordModal"; +export * from "./DNSProviderCreateModal"; +// export * from "./DNSProviderEditModal.tsx.disabled"; +export * from "./HostCreateModal"; +export * from "./ProfileModal"; +export * from "./SetPasswordModal"; +export * from "./UpstreamCreateModal"; +export * from "./UpstreamEditModal"; +export * from "./UpstreamNginxConfigModal"; +export * from "./UserCreateModal"; +export * from "./UserEditModal"; diff --git a/frontend/src/modules/Acmesh.ts b/frontend/src/modules/Acmesh.ts new file mode 100644 index 000000000..2ba99d3ad --- /dev/null +++ b/frontend/src/modules/Acmesh.ts @@ -0,0 +1,74 @@ +/* eslint @typescript-eslint/naming-convention: off */ +export const acmeshProviders: any = { + dns_cf: "Cloudflare", + dns_dp: "DNSPod.cn", + dns_cx: "CloudXNS.com", + dns_gd: "GoDaddy.com", + dns_pdns: "PowerDNS", + dns_nsupdate: "nsupdate", + dns_lua: "LuaDNS", + dns_me: "DNSMadeEasy", + dns_aws: "Amazon Route53", + dns_ali: "Aliyun", + dns_ispconfig: "ISPConfig", + dns_ad: "Alwaysdata", + dns_linode_v4: "Linode", + dns_freedns: "FreeDNS", + dns_cyon: "cyon.ch", + dns_do: "Domain-Offensive", + dns_gandi_livedns: "Gandi LiveDNS", + dns_knot: "Knot", + dns_dgon: "DigitalOcean", + dns_cloudns: "ClouDNS.net", + dns_infoblox: "Infoblox", + dns_vscale: "VSCALE", + dns_dynu: "Dynu", + dns_dnsimple: "DNSimple", + dns_nsone: "NS1.com", + dns_duckdns: "DuckDNS.org", + dns_namecom: "Name.com", + dns_dyn: "Dyn", + dns_yandex: "pdd.yandex.ru", + dns_he: "Hurricane Electric", + dns_unoeuro: "UnoEuro", + dns_inwx: "INWX", + dns_servercow: "Servercow", + dns_namesilo: "Namesilo.com", + dns_autodns: "autoDNS", + dns_azure: "Azure", + dns_selectel: "Selectel", + dns_zonomi: "zonomi.com", + dns_dreamhost: "DreamHost", + dns_da: "DirectAdmin", + dns_kinghost: "KingHost", + dns_zilore: "Zilore", + dns_loopia: "Loopia", + dns_acmedns: "ACME DNS", + dns_tele3: "TELE3", + dns_euserv: "Euserv.eu", + dns_dpi: "DNSPod.com", + dns_gcloud: "Google Cloud", + dns_conoha: "ConoHa", + dns_netcup: "netcup", + dns_gdnsdk: "GratisDNS.dk", + dns_namecheap: "Namecheap", + dns_mydnsjp: "MyDNS.JP", + dns_hostingde: "hosting.de", + dns_neodigit: "Neodigit.net", + dns_exoscale: "Exoscale", + dns_active24: "Active24", + dns_doapi: "do.de", + dns_nw: "Nexcess", + dns_rackspace: "Rackspace", + dns_online: "Online.net", + dns_mydevil: "MyDevil.net", + dns_cn: "Core-Networks", + // more, from #68 on this: https://github.com/acmesh-official/acme.sh/wiki/dnsapi +}; + +export default function getNiceDNSProvider(acmeshName: string) { + if (typeof acmeshProviders[acmeshName] !== "undefined") { + return acmeshProviders[acmeshName]; + } + return acmeshName; +} diff --git a/frontend/src/modules/AuthStore.ts b/frontend/src/modules/AuthStore.ts new file mode 100644 index 000000000..a4036031b --- /dev/null +++ b/frontend/src/modules/AuthStore.ts @@ -0,0 +1,84 @@ +import { TokenResponse } from "src/api/npm"; + +export const TOKEN_KEY = "authentications"; + +export class AuthStore { + // Get all tokens from stack + get tokens() { + const t = localStorage.getItem(TOKEN_KEY); + let tokens = []; + if (t !== null) { + try { + tokens = JSON.parse(t); + } catch (e) { + // do nothing + } + } + return tokens; + } + + // Get last token from stack + get token() { + const t = this.tokens; + if (t.length) { + return t[t.length - 1]; + } + return null; + } + + // Get expires from last token + get expires() { + const t = this.token; + if (t && typeof t.expires !== "undefined") { + const expires = Number(t.expires); + if (expires && !isNaN(expires)) { + return expires; + } + } + return null; + } + + // Filter out invalid tokens and return true if we find one that is valid + hasActiveToken() { + const t = this.tokens; + if (!t.length) { + return false; + } + + const now = Math.round(new Date().getTime() / 1000); + const oneMinuteBuffer = 60; + for (let i = t.length - 1; i >= 0; i--) { + const valid = t[i].expires - oneMinuteBuffer > now; + if (valid) { + return true; + } else { + this.drop(); + } + } + return false; + } + + // Set a single token on the stack + set({ token, expires }: TokenResponse) { + localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }])); + } + + // Add a token to the stack + add({ token, expires }: TokenResponse) { + const t = this.tokens; + t.push({ token, expires }); + localStorage.setItem(TOKEN_KEY, t); + } + + // Drop a token from the stack + drop() { + const t = this.tokens; + localStorage.setItem(TOKEN_KEY, t.splice(-1, 1)); + } + + clear() { + localStorage.removeItem(TOKEN_KEY); + } +} + +export default new AuthStore(); diff --git a/frontend/src/modules/Validations.tsx b/frontend/src/modules/Validations.tsx new file mode 100644 index 000000000..ab7625312 --- /dev/null +++ b/frontend/src/modules/Validations.tsx @@ -0,0 +1,63 @@ +import { intl } from "src/locale"; + +const validateString = (minLength = 0, maxLength = 0) => { + if (minLength <= 0 && maxLength <= 0) { + // this doesn't require translation + console.error( + "validateString() must be called with a min or max or both values in order to work!", + ); + } + + return (value: string): string | undefined => { + if (minLength && !value.length) { + return intl.formatMessage({ id: "form.required" }); + } + if (minLength && value.length < minLength) { + return intl.formatMessage( + { id: "form.min-length" }, + { count: minLength }, + ); + } + if (maxLength && value.length > maxLength) { + return intl.formatMessage( + { id: "form.max-length" }, + { count: maxLength }, + ); + } + }; +}; + +const validateNumber = (min = -1, max = -1) => { + if (min === -1 && max === -1) { + // this doesn't require translation + console.error( + "validateNumber() must be called with a min or max or both values in order to work!", + ); + } + + return (value: string): string | undefined => { + const int: number = +value; + if (min > -1 && !int) { + return intl.formatMessage({ id: "form.required" }); + } + if (min > -1 && int < min) { + return intl.formatMessage({ id: "form.min-int" }, { count: min }); + } + if (max > -1 && int > max) { + return intl.formatMessage({ id: "form.max-int" }, { count: max }); + } + }; +}; + +const validateEmail = () => { + return (value: string): string | undefined => { + if (!value.length) { + return intl.formatMessage({ id: "form.required" }); + } + if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(value)) { + return intl.formatMessage({ id: "form.invalid-email" }); + } + }; +}; + +export { validateEmail, validateNumber, validateString }; diff --git a/frontend/src/pages/AccessLists/Table.tsx b/frontend/src/pages/AccessLists/Table.tsx new file mode 100644 index 000000000..ca21c192a --- /dev/null +++ b/frontend/src/pages/AccessLists/Table.tsx @@ -0,0 +1,138 @@ +import { useEffect, useMemo } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + IDFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const [columns, tableData] = useMemo(() => { + const columns: any = [ + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + sortable: true, + }, + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + show: (data: any) => !data.isSystem, + }, + ]), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export default Table; diff --git a/frontend/src/pages/AccessLists/TableWrapper.tsx b/frontend/src/pages/AccessLists/TableWrapper.tsx new file mode 100644 index 000000000..47da902a8 --- /dev/null +++ b/frontend/src/pages/AccessLists/TableWrapper.tsx @@ -0,0 +1,98 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { + EmptyList, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "src/components"; +import { useAccessLists } from "src/hooks"; +import { intl } from "src/locale"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +interface TableWrapperProps { + onCreateClick?: () => void; +} +function TableWrapper({ onCreateClick }: TableWrapperProps) { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useAccessLists( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + {intl.formatMessage({ id: "lets-go" })} + + } + /> + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( + + ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/AccessLists/index.tsx b/frontend/src/pages/AccessLists/index.tsx new file mode 100644 index 000000000..6296d40e7 --- /dev/null +++ b/frontend/src/pages/AccessLists/index.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { HelpDrawer, PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { AccessListCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function AccessLists() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + + {intl.formatMessage({ id: "access-lists.title" })} + + + + setCreateShown(true)}> + {intl.formatMessage({ id: "access-list.create" })} + + + + setCreateShown(true)} /> + setCreateShown(false)} + /> + + ); +} + +export default AccessLists; diff --git a/frontend/src/pages/AuditLog/index.tsx b/frontend/src/pages/AuditLog/index.tsx new file mode 100644 index 000000000..9752a1b98 --- /dev/null +++ b/frontend/src/pages/AuditLog/index.tsx @@ -0,0 +1,11 @@ +import { Heading } from "@chakra-ui/react"; + +import { intl } from "src/locale"; + +function AuditLog() { + return ( + {intl.formatMessage({ id: "audit-log.title" })} + ); +} + +export default AuditLog; diff --git a/frontend/src/pages/CertificateAuthorities/Table.tsx b/frontend/src/pages/CertificateAuthorities/Table.tsx new file mode 100644 index 000000000..22e20eb96 --- /dev/null +++ b/frontend/src/pages/CertificateAuthorities/Table.tsx @@ -0,0 +1,156 @@ +import { useEffect, useMemo, useState } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + BooleanFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; +import { CertificateAuthorityEditModal } from "src/modals"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const [editId, setEditId] = useState(0); + const [columns, tableData] = useMemo(() => { + const columns = [ + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.max-domains" }), + accessor: "maxDomains", + sortable: true, + }, + { + Header: intl.formatMessage({ id: "column.wildcard-support" }), + accessor: "isWildcardSupported", + Cell: BooleanFormatter(), + sortable: true, + }, + { + id: "actions", + accessor: "id", + className: "w-80", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ + id: "action.edit", + }), + onClick: (_: any, { id }: any) => setEditId(id), + icon: , + disabled: (data: any) => data.isReadonly, + }, + ]), + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ( + <> + + {editId ? ( + setEditId(0)} + /> + ) : null} + + ); +} + +export default Table; diff --git a/frontend/src/pages/CertificateAuthorities/TableWrapper.tsx b/frontend/src/pages/CertificateAuthorities/TableWrapper.tsx new file mode 100644 index 000000000..cf0c17c82 --- /dev/null +++ b/frontend/src/pages/CertificateAuthorities/TableWrapper.tsx @@ -0,0 +1,75 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { SpinnerPage, tableEventReducer } from "src/components"; +import { useCertificateAuthorities } from "src/hooks"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +function TableWrapper() { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = + useCertificateAuthorities(offset, limit, sortBy, filters); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (typeof data?.total === "undefined") { + return ( + + + There was an error fetching the data. + + ); + } + + const pagination = { + offset: data?.offset, + limit: data?.limit, + total: data?.total, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/CertificateAuthorities/index.tsx b/frontend/src/pages/CertificateAuthorities/index.tsx new file mode 100644 index 000000000..3328ebbdb --- /dev/null +++ b/frontend/src/pages/CertificateAuthorities/index.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { HelpDrawer, PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { CertificateAuthorityCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function CertificateAuthorities() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + + {intl.formatMessage({ id: "certificate-authorities.title" })} + + + + setCreateShown(true)}> + {intl.formatMessage({ id: "certificate-authority.create" })} + + + + + setCreateShown(false)} + /> + + ); +} + +export default CertificateAuthorities; diff --git a/frontend/src/pages/Certificates/Table.tsx b/frontend/src/pages/Certificates/Table.tsx new file mode 100644 index 000000000..f1b4f9695 --- /dev/null +++ b/frontend/src/pages/Certificates/Table.tsx @@ -0,0 +1,212 @@ +import { useEffect, useMemo, useState } from "react"; + +import { FiDownload, FiEdit, FiRefreshCw, FiTrash2 } from "react-icons/fi"; +import { useFilters, usePagination, useSortBy, useTable } from "react-table"; + +import { + ActionsFormatter, + CertificateStatusFormatter, + CertificateTypeFormatter, + DomainsFormatter, + GravatarFormatter, + IDFormatter, + MonospaceFormatter, + tableEvents, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; +import { CertificateEditModal } from "src/modals"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; + onRenewal: (id: number) => void; + onDelete: (id: number) => void; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, + onRenewal, + onDelete, +}: TableProps) { + const [editId, setEditId] = useState(0); + const [columns, tableData] = useMemo(() => { + const columns = [ + { + accessor: "user.gravatarUrl", + Cell: GravatarFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + className: "w-80", + sortable: true, + }, + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + Cell: MonospaceFormatter(), + }, + { + Header: intl.formatMessage({ id: "column.domain-names" }), + accessor: "domainNames", + sortable: true, + Filter: TextFilter, + Cell: DomainsFormatter(), + }, + { + Header: intl.formatMessage({ id: "column.type" }), + accessor: "type", + sortable: true, + Cell: CertificateTypeFormatter(), + }, + { + Header: intl.formatMessage({ id: "column.status" }), + accessor: "status", + sortable: true, + Cell: CertificateStatusFormatter(), + }, + { + id: "actions", + accessor: "id", + className: "w-80", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ + id: "action.edit", + }), + onClick: (_: any, { id }: any) => alert(id), + icon: , + disabled: (data: any) => + data.type === "dns" || data.type === "http", + }, + { + title: intl.formatMessage({ + id: "action.renew", + }), + onClick: (_: any, { id }: any) => onRenewal(id), + icon: , + disabled: (data: any) => + data.type !== "dns" && data.type !== "http", + }, + { + title: intl.formatMessage({ + id: "action.download", + }), + onClick: (_: any, { id }: any) => alert(id), + icon: , + disabled: (data: any) => data.isReadonly, + }, + { + title: intl.formatMessage({ + id: "action.delete", + }), + onClick: (_: any, { id }: any) => onDelete(id), + icon: , + disabled: (data: any) => data.isReadonly, + }, + ]), + }, + ]; + return [columns, data]; + }, [data, onRenewal, onDelete]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ( + <> + + {editId ? ( + setEditId(0)} + /> + ) : null} + + ); +} + +export default Table; diff --git a/frontend/src/pages/Certificates/TableWrapper.tsx b/frontend/src/pages/Certificates/TableWrapper.tsx new file mode 100644 index 000000000..584e163ff --- /dev/null +++ b/frontend/src/pages/Certificates/TableWrapper.tsx @@ -0,0 +1,139 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon, useToast } from "@chakra-ui/react"; +import { useQueryClient } from "@tanstack/react-query"; + +import { renewCertificate, deleteCertificate } from "src/api/npm"; +import { EmptyList, SpinnerPage, tableEventReducer } from "src/components"; +import { useCertificates } from "src/hooks"; +import { intl } from "src/locale"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; +function TableWrapper() { + const toast = useToast(); + const queryClient = useQueryClient(); + + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useCertificates( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + const renewCert = async (id: number) => { + try { + await renewCertificate(id); + toast({ + description: intl.formatMessage({ + id: `certificate.renewal-requested`, + }), + status: "info", + position: "top", + duration: 3000, + isClosable: true, + }); + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, 500); + } catch (err: any) { + toast({ + description: err.message, + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + } + }; + + const deleteCert = async (id: number) => { + try { + await deleteCertificate(id); + toast({ + description: intl.formatMessage({ + id: `certificate.deleted`, + }), + status: "success", + position: "top", + duration: 3000, + isClosable: true, + }); + setTimeout(() => { + queryClient.invalidateQueries({ queryKey: ["certificates"] }); + }, 500); + } catch (err: any) { + toast({ + description: err.message, + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + } + }; + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/Certificates/index.tsx b/frontend/src/pages/Certificates/index.tsx new file mode 100644 index 000000000..1a6003261 --- /dev/null +++ b/frontend/src/pages/Certificates/index.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; + +import { + Heading, + HStack, + Menu, + MenuList, + MenuItem, + MenuDivider, +} from "@chakra-ui/react"; +import { FiGlobe, FiServer, FiShieldOff, FiUpload } from "react-icons/fi"; + +import { HelpDrawer, PrettyMenuButton } from "src/components"; +import { useDNSProviders } from "src/hooks"; +import { intl } from "src/locale"; +import { CertificateCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function Certificates() { + const [createShown, setCreateShown] = useState(""); + const { data: dnsProviders, isLoading: dnsProvidersIsLoading } = + useDNSProviders(0, 999); + + return ( + <> + + + {intl.formatMessage({ id: "certificates.title" })} + + + + + + {intl.formatMessage({ id: "certificate.create" })} + + + } + onClick={() => setCreateShown("http")}> + {intl.formatMessage({ id: "type.http" })} + + } + onClick={() => setCreateShown("dns")}> + {intl.formatMessage({ id: "type.dns" })} + + + } + onClick={() => setCreateShown("custom")}> + {intl.formatMessage({ id: "type.custom" })} + + } + onClick={() => setCreateShown("mkcert")}> + {intl.formatMessage({ id: "type.mkcert" })} + + + + + + + setCreateShown("")} + /> + + ); +} + +export default Certificates; diff --git a/frontend/src/pages/DNSProviders/Table.tsx b/frontend/src/pages/DNSProviders/Table.tsx new file mode 100644 index 000000000..aee833d77 --- /dev/null +++ b/frontend/src/pages/DNSProviders/Table.tsx @@ -0,0 +1,153 @@ +import { useEffect, useMemo } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + DNSProviderFormatter, + IDFormatter, + SecondsFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const [columns, tableData] = useMemo(() => { + const columns: any = [ + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + sortable: true, + }, + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.acmesh-name" }), + accessor: "acmeshName", + Cell: DNSProviderFormatter(), + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.dns-sleep" }), + accessor: "dnsSleep", + Cell: SecondsFormatter(), + sortable: true, + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + show: (data: any) => !data.isSystem, + }, + ]), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export default Table; diff --git a/frontend/src/pages/DNSProviders/TableWrapper.tsx b/frontend/src/pages/DNSProviders/TableWrapper.tsx new file mode 100644 index 000000000..48e66188c --- /dev/null +++ b/frontend/src/pages/DNSProviders/TableWrapper.tsx @@ -0,0 +1,98 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { + EmptyList, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "src/components"; +import { useDNSProviders } from "src/hooks"; +import { intl } from "src/locale"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +interface TableWrapperProps { + onCreateClick: () => void; +} +function TableWrapper({ onCreateClick }: TableWrapperProps) { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useDNSProviders( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + {intl.formatMessage({ id: "lets-go" })} + + } + /> + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/DNSProviders/index.tsx b/frontend/src/pages/DNSProviders/index.tsx new file mode 100644 index 000000000..2952dbdf4 --- /dev/null +++ b/frontend/src/pages/DNSProviders/index.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { HelpDrawer, PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { DNSProviderCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function DNSProviders() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + + {intl.formatMessage({ id: "dns-providers.title" })} + + + + setCreateShown(true)}> + {intl.formatMessage({ id: "create-dns-provider" })} + + + + setCreateShown(true)} /> + setCreateShown(false)} + /> + + ); +} + +export default DNSProviders; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx new file mode 100644 index 000000000..95ba1f37a --- /dev/null +++ b/frontend/src/pages/Dashboard/index.tsx @@ -0,0 +1,11 @@ +import { Heading } from "@chakra-ui/react"; + +import { intl } from "src/locale"; + +function Dashboard() { + return ( + {intl.formatMessage({ id: "dashboard.title" })} + ); +} + +export default Dashboard; diff --git a/frontend/src/pages/Hosts/Table.tsx b/frontend/src/pages/Hosts/Table.tsx new file mode 100644 index 000000000..e99fc5dc7 --- /dev/null +++ b/frontend/src/pages/Hosts/Table.tsx @@ -0,0 +1,163 @@ +import { useEffect, useMemo } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + DomainsFormatter, + GravatarFormatter, + HostStatusFormatter, + HostTypeFormatter, + IDFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const [columns, tableData] = useMemo(() => { + const columns: any = [ + { + accessor: "user.gravatarUrl", + Cell: GravatarFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.domain-names" }), + accessor: "domainNames", + Cell: DomainsFormatter(), + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.type" }), + accessor: "type", + Cell: HostTypeFormatter(), + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.validation-type" }), + accessor: "certificate.type", + }, + { + Header: intl.formatMessage({ id: "column.status" }), + accessor: "isDisabled", + Cell: HostStatusFormatter(), + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + }, + ]), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export default Table; diff --git a/frontend/src/pages/Hosts/TableWrapper.tsx b/frontend/src/pages/Hosts/TableWrapper.tsx new file mode 100644 index 000000000..3482508a9 --- /dev/null +++ b/frontend/src/pages/Hosts/TableWrapper.tsx @@ -0,0 +1,98 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { + EmptyList, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "src/components"; +import { useHosts } from "src/hooks"; +import { intl } from "src/locale"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "domain_names", + desc: false, + }, + ], + filters: [], +}; + +interface TableWrapperProps { + onCreateClick?: () => void; +} +function TableWrapper({ onCreateClick }: TableWrapperProps) { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useHosts( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + {intl.formatMessage({ id: "lets-go" })} + + } + /> + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/Hosts/index.tsx b/frontend/src/pages/Hosts/index.tsx new file mode 100644 index 000000000..bfed3f38c --- /dev/null +++ b/frontend/src/pages/Hosts/index.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { HelpDrawer, PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { HostCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function Hosts() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + {intl.formatMessage({ id: "hosts.title" })} + + + setCreateShown(true)}> + {intl.formatMessage({ id: "host.create" })} + + + + setCreateShown(true)} /> + setCreateShown(false)} + /> + + ); +} + +export default Hosts; diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx new file mode 100644 index 000000000..c546195ce --- /dev/null +++ b/frontend/src/pages/Login/index.tsx @@ -0,0 +1,167 @@ +import { useEffect, useRef } from "react"; + +import { + Box, + Center, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Input, + Stack, + useColorModeValue, + useToast, +} from "@chakra-ui/react"; +import { Field, Form, Formik } from "formik"; + +import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { intl } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; + +// import logo from "../../img/logo-256.png"; + +function Login() { + const toast = useToast(); + const emailRef = useRef(null); + const { login } = useAuthState(); + + const onSubmit = async (values: any, { setSubmitting }: any) => { + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + try { + await login("local", values.email, values.password); + } catch (err) { + if (err instanceof Error) { + showErr(err.message); + } + } + setSubmitting(false); + }; + + useEffect(() => { + // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. + emailRef.current.focus(); + }, []); + + return ( + + + + + + + + +
+ Logo +
+
+ + + {({ isSubmitting }) => ( +
+ + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.email" })} + + + + {form.errors.email} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ + id: "user.password", + })} + + + + {form.errors.password} + + + )} + + + {/* + + + {intl.formatMessage({ + id: "login.forgot", + })} + + + */} + + {intl.formatMessage({ + id: "login.login", + })} + + + + + )} +
+
+
+
+ +
+ ); +} + +export default Login; diff --git a/frontend/src/pages/NginxTemplates/NginxTemplatesTable.tsx b/frontend/src/pages/NginxTemplates/NginxTemplatesTable.tsx new file mode 100644 index 000000000..4a136e345 --- /dev/null +++ b/frontend/src/pages/NginxTemplates/NginxTemplatesTable.tsx @@ -0,0 +1,149 @@ +import { useEffect, useMemo } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + HostTypeFormatter, + IDFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; + +const rowActions = [ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + show: (data: any) => !data.isSystem, + }, +]; + +export interface NginxTemplatesTableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function NginxTemplatesTable({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: NginxTemplatesTableProps) { + const [columns, tableData] = useMemo(() => { + const columns: any[] = [ + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + className: "w-80", + sortable: true, + }, + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.type" }), + accessor: "type", + Cell: HostTypeFormatter(), + sortable: true, + Filter: TextFilter, + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter(rowActions), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export { NginxTemplatesTable }; diff --git a/frontend/src/pages/NginxTemplates/index.tsx b/frontend/src/pages/NginxTemplates/index.tsx new file mode 100644 index 000000000..0a6ac451e --- /dev/null +++ b/frontend/src/pages/NginxTemplates/index.tsx @@ -0,0 +1,89 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon, Heading, HStack } from "@chakra-ui/react"; + +import { + HelpDrawer, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "src/components"; +import { useNginxTemplates } from "src/hooks"; +import { intl } from "src/locale"; + +import { NginxTemplatesTable } from "./NginxTemplatesTable"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +function NginxTemplates() { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, error, data } = useNginxTemplates( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (error || (!tableData && !isFetching && !isLoading)) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( + <> + + + {intl.formatMessage({ id: "nginx-templates.title" })} + + + + + {intl.formatMessage({ id: "create-nginx-template" })} + + + + + + ); +} + +export default NginxTemplates; diff --git a/frontend/src/pages/Settings/SettingsTable.tsx b/frontend/src/pages/Settings/SettingsTable.tsx new file mode 100644 index 000000000..7d19f9654 --- /dev/null +++ b/frontend/src/pages/Settings/SettingsTable.tsx @@ -0,0 +1,139 @@ +import { useEffect, useMemo } from "react"; + +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { intl } from "src/locale"; + +const rowActions = [ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + show: (data: any) => !data.isSystem, + }, +]; + +export interface SettingsTableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function SettingsTable({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: SettingsTableProps) { + const [columns, tableData] = useMemo(() => { + const columns = [ + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.description" }), + accessor: "description", + sortable: true, + Filter: TextFilter, + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter(rowActions), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export { SettingsTable }; diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx new file mode 100644 index 000000000..354492b79 --- /dev/null +++ b/frontend/src/pages/Settings/index.tsx @@ -0,0 +1,74 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon, Heading } from "@chakra-ui/react"; + +import { SpinnerPage, tableEventReducer } from "src/components"; +import { useSettings } from "src/hooks"; +import { intl } from "src/locale"; + +import { SettingsTable } from "./SettingsTable"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +function Settings() { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, error, data } = useSettings( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (error || (!tableData && !isFetching && !isLoading)) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( + <> + {intl.formatMessage({ id: "settings.title" })} + + + ); +} + +export default Settings; diff --git a/frontend/src/pages/Setup/index.tsx b/frontend/src/pages/Setup/index.tsx new file mode 100644 index 000000000..8b4980427 --- /dev/null +++ b/frontend/src/pages/Setup/index.tsx @@ -0,0 +1,216 @@ +import { useEffect, useRef } from "react"; + +import { + Box, + Center, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + Input, + Stack, + useColorModeValue, + useToast, +} from "@chakra-ui/react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Field, Form, Formik } from "formik"; + +import { createUser } from "src/api/npm"; +// import logo from "src/assets/logo-256.png"; +import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components"; +import { useAuthState } from "src/context"; +import { intl } from "src/locale"; +import { validateEmail, validateString } from "src/modules/Validations"; + +interface Payload { + name: string; + email: string; + password: string; +} + +function Setup() { + const toast = useToast(); + const nameRef = useRef(null); + const queryClient = useQueryClient(); + const { login } = useAuthState(); + + const onSubmit = async (values: Payload, { setSubmitting }: any) => { + const { password, ...payload } = { + ...values, + ...{ + isDisabled: false, + auth: { + type: "local", + secret: values.password, + }, + capabilities: ["full-admin"], + }, + }; + + const showErr = (msg: string) => { + toast({ + description: intl.formatMessage({ + id: `error.${msg}`, + }), + status: "error", + position: "top", + duration: 3000, + isClosable: true, + }); + }; + + try { + const response = await createUser(payload); + if (response && typeof response.id !== "undefined" && response.id) { + try { + await login("local", response.email, password); + // Trigger a Health change + await queryClient.refetchQueries({ queryKey: ["health"] }); + // window.location.reload(); + } catch (err: any) { + showErr(err.message); + } + } else { + showErr("cannot_create_user"); + } + } catch (err: any) { + showErr(err.message); + } + setSubmitting(false); + }; + + useEffect(() => { + // @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'. + nameRef.current.focus(); + }, []); + + return ( + + + + + + + + + +
+ Logo +
+
+ + + + {intl.formatMessage({ id: "setup.title" })} + + + + + {({ isSubmitting }) => ( +
+ + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.name" })} + + + + {form.errors.name} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.email" })} + + + + {form.errors.email} + + + )} + + + {({ field, form }: any) => ( + + + {intl.formatMessage({ id: "user.password" })} + + + + {form.errors.password} + + + )} + + + + {intl.formatMessage({ id: "setup.create" })} + + + )} +
+
+
+
+
+ +
+ ); +} + +export default Setup; diff --git a/frontend/src/pages/Upstreams/Table.tsx b/frontend/src/pages/Upstreams/Table.tsx new file mode 100644 index 000000000..0a5714069 --- /dev/null +++ b/frontend/src/pages/Upstreams/Table.tsx @@ -0,0 +1,178 @@ +import { useEffect, useMemo, useState } from "react"; + +import { FiEdit, FiHardDrive } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + GravatarFormatter, + IDFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, + UpstreamStatusFormatter, +} from "src/components"; +import { intl } from "src/locale"; +import { UpstreamEditModal, UpstreamNginxConfigModal } from "src/modals"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const [editId, setEditId] = useState(0); + const [configId, setConfigId] = useState(0); + const [columns, tableData] = useMemo(() => { + const columns: any = [ + { + accessor: "user.gravatarUrl", + Cell: GravatarFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.servers" }), + accessor: "servers.length", + }, + { + Header: intl.formatMessage({ id: "column.status" }), + accessor: "status", + Cell: UpstreamStatusFormatter(), + sortable: true, + }, + { + id: "actions", + accessor: "id", + className: "w-80", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (_: any, { id }: any) => setEditId(id), + icon: , + }, + { + title: intl.formatMessage({ id: "action.nginx-config" }), + onClick: (_: any, { id }: any) => setConfigId(id), + icon: , + }, + ]), + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ( + <> + + {editId ? ( + setEditId(0)} + /> + ) : null} + {configId ? ( + setConfigId(0)} + /> + ) : null} + + ); +} + +export default Table; diff --git a/frontend/src/pages/Upstreams/TableWrapper.tsx b/frontend/src/pages/Upstreams/TableWrapper.tsx new file mode 100644 index 000000000..69919e8d2 --- /dev/null +++ b/frontend/src/pages/Upstreams/TableWrapper.tsx @@ -0,0 +1,98 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { + EmptyList, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "src/components"; +import { useUpstreams } from "src/hooks"; +import { intl } from "src/locale"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +interface TableWrapperProps { + onCreateClick: () => void; +} +function TableWrapper({ onCreateClick }: TableWrapperProps) { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useUpstreams( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + {intl.formatMessage({ id: "lets-go" })} + + } + /> + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/Upstreams/index.tsx b/frontend/src/pages/Upstreams/index.tsx new file mode 100644 index 000000000..bbf4fd107 --- /dev/null +++ b/frontend/src/pages/Upstreams/index.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { HelpDrawer, PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { UpstreamCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function Upstreams() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + + {intl.formatMessage({ id: "upstreams.title" })} + + + + setCreateShown(true)}> + {intl.formatMessage({ id: "upstream.create" })} + + + + setCreateShown(true)} /> + setCreateShown(false)} + /> + + ); +} + +export default Upstreams; diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx new file mode 100644 index 000000000..25877008a --- /dev/null +++ b/frontend/src/pages/Users/Table.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect, useMemo } from "react"; + +import { FiEdit, FiLock } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +import { + tableEvents, + ActionsFormatter, + CapabilitiesFormatter, + DisabledFormatter, + GravatarFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "src/components"; +import { useUser } from "src/hooks"; +import { intl } from "src/locale"; +import { SetPasswordModal, UserEditModal } from "src/modals"; + +export interface TableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function Table({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: TableProps) { + const { data: me } = useUser("me"); + const [editId, setEditId] = useState(0); + const [setPasswordUserId, setSetPasswordUserId] = useState(0); + const [columns, tableData] = useMemo(() => { + const columns = [ + { + accessor: "gravatarUrl", + className: "w-80", + Cell: GravatarFormatter(), + }, + { + Header: intl.formatMessage({ id: "user.name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + Cell: DisabledFormatter(), + }, + { + Header: intl.formatMessage({ id: "user.email" }), + accessor: "email", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "user.capabilities" }), + accessor: "capabilities", + Cell: CapabilitiesFormatter(), + }, + { + id: "actions", + accessor: "id", + className: "w-80", + Cell: ActionsFormatter([ + { + title: intl.formatMessage({ id: "action.edit" }), + icon: , + onClick: (_: any, { id }: any) => setEditId(id), + disabled: (data: any) => data.isSystem || data.id === me?.id, + }, + { + title: intl.formatMessage({ id: "action.set-password" }), + icon: , + onClick: (_: any, { id }: any) => setSetPasswordUserId(id), + disabled: (data: any) => data.isSystem || data.id === me?.id, + }, + ]), + }, + ]; + return [columns, data]; + }, [data, me?.id]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const { gotoPage } = tableInstance; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ( + <> + + {editId ? ( + setEditId(0)} /> + ) : null} + + {setPasswordUserId ? ( + setSetPasswordUserId(0)} + /> + ) : null} + + ); +} + +export default Table; diff --git a/frontend/src/pages/Users/TableWrapper.tsx b/frontend/src/pages/Users/TableWrapper.tsx new file mode 100644 index 000000000..9ad515f3e --- /dev/null +++ b/frontend/src/pages/Users/TableWrapper.tsx @@ -0,0 +1,79 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon } from "@chakra-ui/react"; + +import { SpinnerPage, tableEventReducer } from "src/components"; +import { useUsers } from "src/hooks"; + +import Table from "./Table"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +function TableWrapper() { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, isError, error, data } = useUsers( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (isFetching || isLoading || !tableData) { + return ; + } + + if (isError) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (typeof data?.total === "undefined") { + return ( + + + There was an error fetching the data. + + ); + } + + const pagination = { + offset: data?.offset, + limit: data?.limit, + total: data?.total, + }; + + return ( +
+ ); +} + +export default TableWrapper; diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx new file mode 100644 index 000000000..15cda39cc --- /dev/null +++ b/frontend/src/pages/Users/index.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; + +import { Heading, HStack } from "@chakra-ui/react"; + +import { PrettyButton } from "src/components"; +import { intl } from "src/locale"; +import { UserCreateModal } from "src/modals"; + +import TableWrapper from "./TableWrapper"; + +function Users() { + const [createShown, setCreateShown] = useState(false); + + return ( + <> + + {intl.formatMessage({ id: "users.title" })} + setCreateShown(true)}> + {intl.formatMessage({ id: "user.create" })} + + + + setCreateShown(false)} + /> + + ); +} + +export default Users; diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts new file mode 100644 index 000000000..6431bc5fc --- /dev/null +++ b/frontend/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/src/styles/fonts.scss b/frontend/src/styles/fonts.scss new file mode 100644 index 000000000..39683f75c --- /dev/null +++ b/frontend/src/styles/fonts.scss @@ -0,0 +1,47 @@ +/* source-sans-pro-regular - 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-regular.woff2") + format("woff2"), + url("/fonts/source-sans-pro/source-sans-pro-v14-latin-regular.woff") + format("woff"); +} + +/* source-sans-pro-italic - 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-italic.woff2") + format("woff2"), + url("/fonts/source-sans-pro/source-sans-pro-v14-latin-italic.woff") + format("woff"); +} + +/* source-sans-pro-700 - 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-700.woff2") + format("woff2"), + url("/fonts/source-sans-pro/source-sans-pro-v14-latin-700.woff") + format("woff"); +} + +/* source-sans-pro-700italic - 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-700italic.woff2") + format("woff2"), + url("/fonts/source-sans-pro/source-sans-pro-v14-latin-700italic.woff") + format("woff"); +} diff --git a/frontend/src/theme/customTheme.ts b/frontend/src/theme/customTheme.ts new file mode 100644 index 000000000..b4f5ae62c --- /dev/null +++ b/frontend/src/theme/customTheme.ts @@ -0,0 +1,25 @@ +import { + theme as chakraTheme, + ThemeConfig, + extendTheme, +} from "@chakra-ui/react"; + +// declare a variable for fonts and set our fonts +const fonts = { + ...chakraTheme.fonts, + body: `"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`, + heading: `"Source Sans Pro",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`, +}; + +const config: ThemeConfig = { + initialColorMode: "system", +}; + +const shadows = { + ...chakraTheme.shadows, + outline: "none", +}; + +const lightTheme = extendTheme({ fonts, config, shadows }); + +export default lightTheme; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 000000000..0340203a0 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + "baseUrl": ".", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "paths": { + "src/*": [ + "./src/*" + ], + }, + "types": [ + "vite/client" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 000000000..eca66688d --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 000000000..6664c4ef9 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import checker from "vite-plugin-checker"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + checker({ + // e.g. use TypeScript check + typescript: true, + }), + ], + resolve: { + alias: { + src: "/src", + }, + }, + assetsInclude: ["**/*.md", "**/*.png"], +}); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js deleted file mode 100644 index 05350a475..000000000 --- a/frontend/webpack.config.js +++ /dev/null @@ -1,144 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const Visualizer = require('webpack-visualizer-plugin'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const PACKAGE = require('./package.json'); - -module.exports = { - entry: { - main: './js/index.js', - login: './js/login.js' - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: `js/[name].bundle.js?v=${PACKAGE.version}`, - chunkFilename: `js/[name].bundle.[id].js?v=${PACKAGE.version}`, - publicPath: '/' - }, - resolve: { - alias: { - 'tabler-core': 'tabler-ui/dist/assets/js/core', - 'bootstrap': 'tabler-ui/dist/assets/js/vendors/bootstrap.bundle.min', - 'sparkline': 'tabler-ui/dist/assets/js/vendors/jquery.sparkline.min', - 'selectize': 'tabler-ui/dist/assets/js/vendors/selectize.min', - 'tablesorter': 'tabler-ui/dist/assets/js/vendors/jquery.tablesorter.min', - 'vector-map': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-2.0.3.min', - 'vector-map-de': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-de-merc', - 'vector-map-world': 'tabler-ui/dist/assets/js/vendors/jquery-jvectormap-world-mill', - 'circle-progress': 'tabler-ui/dist/assets/js/vendors/circle-progress.min', - 'c3': 'tabler-ui/dist/assets/js/vendors/chart.bundle.min' - } - }, - module: { - rules: [ - // Shims for tabler-ui - { - test: /assets\/js\/core/, - loader: 'imports-loader?bootstrap' - }, - { - test: /jquery-jvectormap-de-merc/, - loader: 'imports-loader?vector-map' - }, - { - test: /jquery-jvectormap-world-mill/, - loader: 'imports-loader?vector-map' - }, - - // other: - { - type: 'javascript/auto', // <= Set the module.type explicitly - test: /\bmessages\.json$/, - loader: 'messageformat-loader', - options: { - biDiSupport: false, - disablePluralKeyChecks: false, - formatters: null, - intlSupport: false, - locale: ['en'], - strictNumberSign: false - } - }, - { - test: /\.js$/, - exclude: /node_modules/, - use: { - loader: 'babel-loader' - } - }, - { - test: /\.ejs$/, - loader: 'ejs-loader' - }, - { - test: /\.scss$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'sass-loader' - ] - }, - { - test: /.*tabler.*\.(jpe?g|gif|png|svg|eot|woff|ttf)$/, - use: [ - { - loader: 'file-loader', - options: { - outputPath: 'assets/tabler-ui/' - } - } - ] - }, - { - test: /source-sans-pro.*\.(woff(2)?)(\?v=\d+\.\d+\.\d+)?$/, - use: [ - { - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'assets/' - } - } - ] - } - ] - }, - plugins: [ - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery', - _: 'underscore' - }), - new HtmlWebpackPlugin({ - template: '!!ejs-webpack-loader!html/index.ejs', - filename: 'index.html', - inject: false, - templateParameters: { - version: PACKAGE.version - } - }), - new HtmlWebpackPlugin({ - template: '!!ejs-webpack-loader!html/login.ejs', - filename: 'login.html', - inject: false, - templateParameters: { - version: PACKAGE.version - } - }), - new MiniCssExtractPlugin({ - filename: 'css/[name].css', - chunkFilename: 'css/[id].css' - }), - new Visualizer({ - filename: '../webpack_stats.html' - }), - new CopyWebpackPlugin([{ - from: 'app-images', - to: 'images', - toType: 'dir', - context: '/app/frontend' - }]) - ] -}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d524ff04e..72527124b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,1820 +2,2273 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - 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/core@^7.9.0": - version "7.11.1" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" - integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" - "@babel/helper-module-transforms" "^7.11.0" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.1" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" - convert-source-map "^1.7.0" +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" + integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== + dependencies: + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" + +"@babel/compat-data@^7.23.5": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" + integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== + +"@babel/core@^7.23.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.5.tgz#15ab5b98e101972d171aeef92ac70d8d6718f06a" + integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.5" + "@babel/helper-compilation-targets" "^7.23.6" + "@babel/helper-module-transforms" "^7.24.5" + "@babel/helpers" "^7.24.5" + "@babel/parser" "^7.24.5" + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.5" + "@babel/types" "^7.24.5" + convert-source-map "^2.0.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" - integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== - dependencies: - "@babel/types" "^7.11.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.5.tgz#e5afc068f932f05616b66713e28d0f04e99daeb3" + integrity sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA== + dependencies: + "@babel/types" "^7.24.5" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-module-imports@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" - integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" - lodash "^4.17.19" - -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-simple-access@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" - integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== - dependencies: - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@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/helpers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== - dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@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" -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" - integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== - -"@babel/template@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" - integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.0" - "@babel/types" "^7.11.0" - debug "^4.1.0" +"@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.3": + version "7.24.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz#6ac476e6d168c7c23ff3ba3cf4f7841d46ac8128" + integrity sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg== + dependencies: + "@babel/types" "^7.24.0" + +"@babel/helper-module-transforms@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz#ea6c5e33f7b262a0ae762fd5986355c45f54a545" + integrity sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.24.3" + "@babel/helper-simple-access" "^7.24.5" + "@babel/helper-split-export-declaration" "^7.24.5" + "@babel/helper-validator-identifier" "^7.24.5" + +"@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz#a924607dd254a65695e5bd209b98b902b3b2f11a" + integrity sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ== + +"@babel/helper-simple-access@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz#50da5b72f58c16b07fbd992810be6049478e85ba" + integrity sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ== + dependencies: + "@babel/types" "^7.24.5" + +"@babel/helper-split-export-declaration@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz#b9a67f06a46b0b339323617c8c6213b9055a78b6" + integrity sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q== + dependencies: + "@babel/types" "^7.24.5" + +"@babel/helper-string-parser@^7.24.1": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" + integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== + +"@babel/helper-validator-identifier@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz#918b1a7fa23056603506370089bd990d8720db62" + integrity sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA== + +"@babel/helper-validator-option@^7.23.5": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" + integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== + +"@babel/helpers@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.5.tgz#fedeb87eeafa62b621160402181ad8585a22a40a" + integrity sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q== + dependencies: + "@babel/template" "^7.24.0" + "@babel/traverse" "^7.24.5" + "@babel/types" "^7.24.5" + +"@babel/highlight@^7.24.2": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.5.tgz#bc0613f98e1dd0720e99b2a9ee3760194a704b6e" + integrity sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.5" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.0", "@babel/parser@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.5.tgz#4a4d5ab4315579e5398a82dcf636ca80c3392790" + integrity sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg== + +"@babel/plugin-transform-react-jsx-self@^7.23.3": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.5.tgz#22cc7572947895c8e4cd034462e65d8ecf857756" + integrity sha512-RtCJoUO2oYrYwFPtR1/jkoBEcFuI1ae9a9IMxeyAVa3a1Ap4AnxmyIKG2b2FaJKqkidw/0cxRbWN+HOs6ZWd1w== + dependencies: + "@babel/helper-plugin-utils" "^7.24.5" + +"@babel/plugin-transform-react-jsx-source@^7.23.3": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.1.tgz#a2dedb12b09532846721b5df99e52ef8dc3351d0" + integrity sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.0" + +"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.24.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" + integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.24.0" + "@babel/types" "^7.24.0" + +"@babel/traverse@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.5.tgz#972aa0bc45f16983bf64aa1f877b2dd0eea7e6f8" + integrity sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA== + dependencies: + "@babel/code-frame" "^7.24.2" + "@babel/generator" "^7.24.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.24.5" + "@babel/parser" "^7.24.5" + "@babel/types" "^7.24.5" + debug "^4.3.1" globals "^11.1.0" - lodash "^4.17.19" -"@babel/types@^7.10.4", "@babel/types@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" - integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.0", "@babel/types@^7.24.5": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.5.tgz#7661930afc638a5383eb0c4aee59b74f38db84d7" + integrity sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" + "@babel/helper-string-parser" "^7.24.1" + "@babel/helper-validator-identifier" "^7.24.5" to-fast-properties "^2.0.0" -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@chakra-ui/accordion@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-2.3.1.tgz#a326509e286a5c4e8478de9bc2b4b05017039e6b" + integrity sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag== + dependencies: + "@chakra-ui/descendant" "3.1.0" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/transition" "2.1.0" + +"@chakra-ui/alert@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/alert/-/alert-2.2.2.tgz#aeba951d120c7c6e69d5f515a695ad6e4db43ffe" + integrity sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw== dependencies: - "@nodelib/fs.stat" "2.0.3" - run-parallel "^1.1.9" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/spinner" "2.1.0" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@chakra-ui/anatomy@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/anatomy/-/anatomy-2.2.2.tgz#2d0e14cba2534d92077ca28abf8c183b6e27897b" + integrity sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg== -"@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== +"@chakra-ui/avatar@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/avatar/-/avatar-2.3.0.tgz#f018a2714d1e3ba5970bcf66558887925fdfccf4" + integrity sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g== dependencies: - "@nodelib/fs.scandir" "2.1.3" - fastq "^1.6.0" + "@chakra-ui/image" "2.1.0" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@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== +"@chakra-ui/breadcrumb@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/breadcrumb/-/breadcrumb-2.2.0.tgz#751bc48498f3c403f97b5d9aae528ebfd405ef48" + integrity sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA== dependencies: - defer-to-connect "^1.0.1" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@types/anymatch@*": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" - integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== +"@chakra-ui/breakpoint-utils@2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz#750d3712668b69f6e8917b45915cee0e08688eed" + integrity sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA== + dependencies: + "@chakra-ui/shared-utils" "2.0.5" -"@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== +"@chakra-ui/button@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/button/-/button-2.1.0.tgz#623ed32cc92fc8e52492923e9924791fc6f25447" + integrity sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA== + dependencies: + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/spinner" "2.1.0" -"@types/html-minifier-terser@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" - integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== +"@chakra-ui/card@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/card/-/card-2.2.0.tgz#b5e59dc51c171fced76ea76bf26088803b8bc184" + integrity sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g== + dependencies: + "@chakra-ui/shared-utils" "2.0.5" -"@types/json-schema@^7.0.4": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== +"@chakra-ui/checkbox@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/checkbox/-/checkbox-2.3.2.tgz#4ecb14a2f57b7470d1a58542ca4691c3b105bfa1" + integrity sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g== + dependencies: + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-callback-ref" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/visually-hidden" "2.2.0" + "@zag-js/focus-visible" "0.16.0" + +"@chakra-ui/clickable@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/clickable/-/clickable-2.1.0.tgz#800fa8d10cf45a41fc50a3df32c679a3ce1921c3" + integrity sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw== + dependencies: + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@types/json-schema@^7.0.8": - version "7.0.9" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" - integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@chakra-ui/close-button@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/close-button/-/close-button-2.1.1.tgz#995b245c56eb41465a71d8667840c238618a7b66" + integrity sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw== + dependencies: + "@chakra-ui/icon" "3.2.0" -"@types/minimist@^1.2.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@chakra-ui/color-mode@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/color-mode/-/color-mode-2.2.0.tgz#828d47234c74ba2fb4c5dd63a63331aead20b9f6" + integrity sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg== + dependencies: + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" -"@types/node@*": - version "14.0.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" - integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== +"@chakra-ui/control-box@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/control-box/-/control-box-2.1.0.tgz#0f4586797b3154c02463bc5c106782e70c88f04f" + integrity sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg== -"@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@chakra-ui/counter@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/counter/-/counter-2.1.0.tgz#e413a2f1093a18f847bb7aa240117fde788a59e6" + integrity sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw== + dependencies: + "@chakra-ui/number-utils" "2.0.7" + "@chakra-ui/react-use-callback-ref" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== +"@chakra-ui/css-reset@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/css-reset/-/css-reset-2.3.0.tgz#83e3160a9c2a12431cad0ee27ebfbf3aedc5c9c7" + integrity sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg== -"@types/tapable@*", "@types/tapable@^1.0.5": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" - integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== - -"@types/uglify-js@*": - version "3.9.3" - resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" - integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w== - dependencies: - source-map "^0.6.1" - -"@types/webpack-sources@*": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-1.4.2.tgz#5d3d4dea04008a779a90135ff96fb5c0c9e6292c" - integrity sha512-77T++JyKow4BQB/m9O96n9d/UUHWLQHlcqXb9Vsf4F1+wKNrrlWNFPDLKNT92RJnCSL6CieTc+NDXtCVZswdTw== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.7.3" - -"@types/webpack@^4.41.8": - version "4.41.21" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.21.tgz#cc685b332c33f153bb2f5fc1fa3ac8adeb592dee" - integrity sha512-2j9WVnNrr/8PLAB5csW44xzQSJwS26aOnICsP3pSGCEdsu6KYtfQ6QJsVUKHWRnm1bL7HziJsfh5fHqth87yKA== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - source-map "^0.6.0" - -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== +"@chakra-ui/descendant@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/descendant/-/descendant-3.1.0.tgz#f3b80ed13ffc4bf1d615b3ed5541bd0905375cca" + integrity sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ== dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== +"@chakra-ui/dom-utils@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/dom-utils/-/dom-utils-2.1.0.tgz#d15df89e458ef19756db04c7cfd084eb552454f0" + integrity sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ== -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== +"@chakra-ui/editable@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/editable/-/editable-3.1.0.tgz#065783c2e3389c3bb9ab0582cb50d38e1dc00fa1" + integrity sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg== + dependencies: + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-callback-ref" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-focus-on-pointer-down" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/event-utils@2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz#e6439ba200825a2f15d8f1973d267d1c00a6d1b4" + integrity sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw== + +"@chakra-ui/focus-lock@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/focus-lock/-/focus-lock-2.1.0.tgz#580e5450fe85356987b9a246abaff8333369c667" + integrity sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w== + dependencies: + "@chakra-ui/dom-utils" "2.1.0" + react-focus-lock "^2.9.4" -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== +"@chakra-ui/form-control@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/form-control/-/form-control-2.2.0.tgz#69c771d6406ddac8ab357ae88446cc11827656a4" + integrity sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw== dependencies: - "@webassemblyjs/wast-printer" "1.9.0" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== +"@chakra-ui/hooks@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/hooks/-/hooks-2.2.1.tgz#b86ce5eeaaab877ddcb11a50842d1227306ace28" + integrity sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ== + dependencies: + "@chakra-ui/react-utils" "2.0.12" + "@chakra-ui/utils" "2.0.15" + compute-scroll-into-view "3.0.3" + copy-to-clipboard "3.3.3" -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== +"@chakra-ui/icon@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/icon/-/icon-3.2.0.tgz#92b9454aa0d561b4994bcd6a1b3bb1fdd5c67bef" + integrity sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ== dependencies: - "@webassemblyjs/ast" "1.9.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== +"@chakra-ui/image@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-2.1.0.tgz#6c205f1ca148e3bf58345b0b5d4eb3d959eb9f87" + integrity sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA== + dependencies: + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== +"@chakra-ui/input@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/input/-/input-2.1.2.tgz#0cad49ec372f8f21f2f4f1db365f34b9a708ff9d" + integrity sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/object-utils" "2.1.0" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== +"@chakra-ui/layout@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/layout/-/layout-2.3.1.tgz#0601c5eb91555d24a7015a7c9d4e01fed2698557" + integrity sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg== dependencies: - "@xtuc/ieee754" "^1.2.0" + "@chakra-ui/breakpoint-utils" "2.0.8" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/object-utils" "2.1.0" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== +"@chakra-ui/lazy-utils@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz#363c3fa1d421362790b416ffa595acb835e1ae5b" + integrity sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg== + +"@chakra-ui/live-region@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/live-region/-/live-region-2.1.0.tgz#02b4b1d997075f19a7a9a87187e08c72e82ef0dd" + integrity sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw== + +"@chakra-ui/media-query@3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/media-query/-/media-query-3.3.0.tgz#40f9151dedb6a7af9df3be0474b59a799c92c619" + integrity sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g== dependencies: - "@xtuc/long" "4.2.2" + "@chakra-ui/breakpoint-utils" "2.0.8" + "@chakra-ui/react-env" "3.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== +"@chakra-ui/menu@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/menu/-/menu-2.2.1.tgz#7d9810d435f6b40fa72ed867a33b88a1ef75073f" + integrity sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g== + dependencies: + "@chakra-ui/clickable" "2.1.0" + "@chakra-ui/descendant" "3.1.0" + "@chakra-ui/lazy-utils" "2.0.5" + "@chakra-ui/popper" "3.1.0" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-animation-state" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-disclosure" "2.1.0" + "@chakra-ui/react-use-focus-effect" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-outside-click" "2.2.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/transition" "2.1.0" + +"@chakra-ui/modal@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/modal/-/modal-2.3.1.tgz#524dc32b6b4f545b54ae531dbf6c74e1052ee794" + integrity sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ== + dependencies: + "@chakra-ui/close-button" "2.1.1" + "@chakra-ui/focus-lock" "2.1.0" + "@chakra-ui/portal" "2.1.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/transition" "2.1.0" + aria-hidden "^1.2.3" + react-remove-scroll "^2.5.6" + +"@chakra-ui/number-input@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/number-input/-/number-input-2.1.2.tgz#dda9095fba6a4b89212332db02831b94120da163" + integrity sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA== + dependencies: + "@chakra-ui/counter" "2.1.0" + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-callback-ref" "2.1.0" + "@chakra-ui/react-use-event-listener" "2.1.0" + "@chakra-ui/react-use-interval" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/number-utils@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz#aaee979ca2fb1923a0373a91619473811315db11" + integrity sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg== + +"@chakra-ui/object-utils@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/object-utils/-/object-utils-2.1.0.tgz#a4ecf9cea92f1de09f5531f53ffdc41e0b19b6c3" + integrity sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ== -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== +"@chakra-ui/pin-input@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/pin-input/-/pin-input-2.1.0.tgz#61e6bbf909ec510634307b2861c4f1891a9f8d81" + integrity sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" + "@chakra-ui/descendant" "3.1.0" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== +"@chakra-ui/popover@2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/popover/-/popover-2.2.1.tgz#89cfd29817abcd204da570073c0f2b4d8072c3a3" + integrity sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg== + dependencies: + "@chakra-ui/close-button" "2.1.1" + "@chakra-ui/lazy-utils" "2.0.5" + "@chakra-ui/popper" "3.1.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-animation-state" "2.1.0" + "@chakra-ui/react-use-disclosure" "2.1.0" + "@chakra-ui/react-use-focus-effect" "2.1.0" + "@chakra-ui/react-use-focus-on-pointer-down" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/popper@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/popper/-/popper-3.1.0.tgz#92a9180c6894763af3b22a6003f9a9d958fe2659" + integrity sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@popperjs/core" "^2.9.3" -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== +"@chakra-ui/portal@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/portal/-/portal-2.1.0.tgz#9e7f57424d7041738b6563cac80134561080bd27" + integrity sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== +"@chakra-ui/progress@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/progress/-/progress-2.2.0.tgz#67444ea9779631d7c8395b2c9c78e5634f994999" + integrity sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" + "@chakra-ui/react-context" "2.1.0" -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== +"@chakra-ui/provider@2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/provider/-/provider-2.4.2.tgz#92cb10b6a7df0720e3fa62716dc7cd872ae3ea3d" + integrity sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" + "@chakra-ui/css-reset" "2.3.0" + "@chakra-ui/portal" "2.1.0" + "@chakra-ui/react-env" "3.1.0" + "@chakra-ui/system" "2.6.2" + "@chakra-ui/utils" "2.0.15" -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== +"@chakra-ui/radio@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/radio/-/radio-2.1.2.tgz#66db19c61a2e628aaf5e727027f7c3b4006ea898" + integrity sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w== + dependencies: + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@zag-js/focus-visible" "0.16.0" -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@chakra-ui/react-children-utils@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz#6c480c6a60678fcb75cb7d57107c7a79e5179b92" + integrity sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA== -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== +"@chakra-ui/react-context@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-context/-/react-context-2.1.0.tgz#4858be1d5ff1c8ac0a0ec088d93a3b7f1cbbff99" + integrity sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w== -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== +"@chakra-ui/react-env@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-env/-/react-env-3.1.0.tgz#7d3c1c05a501bb369524d9f3d38c9325eb16ab50" + integrity sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw== + dependencies: + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + +"@chakra-ui/react-types@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-types/-/react-types-2.0.7.tgz#799c166a44882b23059c8f510eac9bd5d0869ac4" + integrity sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ== -acorn-node@^1.2.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== +"@chakra-ui/react-use-animation-state@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.1.0.tgz#eab661fbafd96804fe867b0df0c27e78feefe6e2" + integrity sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg== dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" + "@chakra-ui/dom-utils" "2.1.0" + "@chakra-ui/react-use-event-listener" "2.1.0" -acorn-walk@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +"@chakra-ui/react-use-callback-ref@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.1.0.tgz#a508085f4d9e7d84d4ceffdf5f41745c9ac451d7" + integrity sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ== -acorn@^5.2.1: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== +"@chakra-ui/react-use-controllable-state@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.1.0.tgz#8fb6fa2f45d0c04173582ae8297e604ffdb9c7d9" + integrity sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg== + dependencies: + "@chakra-ui/react-use-callback-ref" "2.1.0" -acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== +"@chakra-ui/react-use-disclosure@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.1.0.tgz#90093eaf45db1bea7a6851dd0ce5cdb3eb66f90a" + integrity sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw== + dependencies: + "@chakra-ui/react-use-callback-ref" "2.1.0" -acorn@^7.0.0, 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== +"@chakra-ui/react-use-event-listener@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.1.0.tgz#afea2645bd9b38f754fc2b8eb858f9bb22385ded" + integrity sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q== + dependencies: + "@chakra-ui/react-use-callback-ref" "2.1.0" -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== +"@chakra-ui/react-use-focus-effect@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.1.0.tgz#963fb790370dfadd51d12666ff2da60706f53a2a" + integrity sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ== + dependencies: + "@chakra-ui/dom-utils" "2.1.0" + "@chakra-ui/react-use-event-listener" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== +"@chakra-ui/react-use-focus-on-pointer-down@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.1.0.tgz#2fbcf6bc7d06d97606747e231a908d5c387ca0cc" + integrity sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg== + dependencies: + "@chakra-ui/react-use-event-listener" "2.1.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.3: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== +"@chakra-ui/react-use-interval@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-interval/-/react-use-interval-2.1.0.tgz#2602c097b3ab74b6644812e4f5efaad621218d98" + integrity sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw== 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" + "@chakra-ui/react-use-callback-ref" "2.1.0" -ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +"@chakra-ui/react-use-latest-ref@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.1.0.tgz#d1e926130102566ece1d39f8a48ed125e0c8441a" + integrity sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ== + +"@chakra-ui/react-use-merge-refs@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.1.0.tgz#c0c233527abdbea9a1348269c192012205762314" + integrity sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ== + +"@chakra-ui/react-use-outside-click@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.2.0.tgz#5570b772a255f6f02b69e967127397c1b5fa3d3c" + integrity sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw== 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" + "@chakra-ui/react-use-callback-ref" "2.1.0" -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= +"@chakra-ui/react-use-pan-event@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.1.0.tgz#51c21bc3c0e9e73d1faef5ea4f7e3c3d071a2758" + integrity sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg== dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" + "@chakra-ui/event-utils" "2.0.8" + "@chakra-ui/react-use-latest-ref" "2.1.0" + framesync "6.1.2" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= +"@chakra-ui/react-use-previous@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-previous/-/react-use-previous-2.1.0.tgz#f6046e6f7398b1e8d7e66ff7ebb8d61c92a2d3d0" + integrity sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg== -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== +"@chakra-ui/react-use-safe-layout-effect@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.1.0.tgz#3a95f0ba6fd5d2d0aa14919160f2c825f13e686f" + integrity sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw== + +"@chakra-ui/react-use-size@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-size/-/react-use-size-2.1.0.tgz#fcf3070eaade8b4a84af8ce5341c4d5ca0a42bec" + integrity sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A== dependencies: - string-width "^3.0.0" + "@zag-js/element-size" "0.10.5" -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== +"@chakra-ui/react-use-timeout@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-timeout/-/react-use-timeout-2.1.0.tgz#24415f54267d7241a3c1d36a5cae4d472834cef7" + integrity sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ== + dependencies: + "@chakra-ui/react-use-callback-ref" "2.1.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== +"@chakra-ui/react-use-update-effect@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.1.0.tgz#5c57cd1f50c2a6a8119e0f57f69510723d69884b" + integrity sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA== + +"@chakra-ui/react-utils@2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz#d6b773b9a5b2e51dce61f51ac8a0e9a0f534f479" + integrity sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw== + dependencies: + "@chakra-ui/utils" "2.0.15" + +"@chakra-ui/react@^2.8.2": + version "2.8.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/react/-/react-2.8.2.tgz#94d692fb35e4447748c5bfd73d8d38a746193c7d" + integrity sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ== + dependencies: + "@chakra-ui/accordion" "2.3.1" + "@chakra-ui/alert" "2.2.2" + "@chakra-ui/avatar" "2.3.0" + "@chakra-ui/breadcrumb" "2.2.0" + "@chakra-ui/button" "2.1.0" + "@chakra-ui/card" "2.2.0" + "@chakra-ui/checkbox" "2.3.2" + "@chakra-ui/close-button" "2.1.1" + "@chakra-ui/control-box" "2.1.0" + "@chakra-ui/counter" "2.1.0" + "@chakra-ui/css-reset" "2.3.0" + "@chakra-ui/editable" "3.1.0" + "@chakra-ui/focus-lock" "2.1.0" + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/hooks" "2.2.1" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/image" "2.1.0" + "@chakra-ui/input" "2.1.2" + "@chakra-ui/layout" "2.3.1" + "@chakra-ui/live-region" "2.1.0" + "@chakra-ui/media-query" "3.3.0" + "@chakra-ui/menu" "2.2.1" + "@chakra-ui/modal" "2.3.1" + "@chakra-ui/number-input" "2.1.2" + "@chakra-ui/pin-input" "2.1.0" + "@chakra-ui/popover" "2.2.1" + "@chakra-ui/popper" "3.1.0" + "@chakra-ui/portal" "2.1.0" + "@chakra-ui/progress" "2.2.0" + "@chakra-ui/provider" "2.4.2" + "@chakra-ui/radio" "2.1.2" + "@chakra-ui/react-env" "3.1.0" + "@chakra-ui/select" "2.1.2" + "@chakra-ui/skeleton" "2.1.0" + "@chakra-ui/skip-nav" "2.1.0" + "@chakra-ui/slider" "2.1.0" + "@chakra-ui/spinner" "2.1.0" + "@chakra-ui/stat" "2.1.1" + "@chakra-ui/stepper" "2.3.1" + "@chakra-ui/styled-system" "2.9.2" + "@chakra-ui/switch" "2.1.2" + "@chakra-ui/system" "2.6.2" + "@chakra-ui/table" "2.1.0" + "@chakra-ui/tabs" "3.0.0" + "@chakra-ui/tag" "3.1.1" + "@chakra-ui/textarea" "2.1.2" + "@chakra-ui/theme" "3.3.1" + "@chakra-ui/theme-utils" "2.0.21" + "@chakra-ui/toast" "7.0.2" + "@chakra-ui/tooltip" "2.3.1" + "@chakra-ui/transition" "2.1.0" + "@chakra-ui/utils" "2.0.15" + "@chakra-ui/visually-hidden" "2.2.0" + +"@chakra-ui/select@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/select/-/select-2.1.2.tgz#f57d6cec0559373c32094fd4a5abd32855829264" + integrity sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA== dependencies: - type-fest "^0.11.0" + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/shared-utils" "2.0.5" -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= +"@chakra-ui/shared-utils@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz#cb2b49705e113853647f1822142619570feba081" + integrity sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q== -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= +"@chakra-ui/skeleton@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/skeleton/-/skeleton-2.1.0.tgz#e3b25dd3afa330029d6d63be0f7cb8d44ad25531" + integrity sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ== + dependencies: + "@chakra-ui/media-query" "3.3.0" + "@chakra-ui/react-use-previous" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -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== +"@chakra-ui/skip-nav@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/skip-nav/-/skip-nav-2.1.0.tgz#cac27eecc6eded1e83c8f0cf7445d727739cb325" + integrity sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug== -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== +"@chakra-ui/slider@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/slider/-/slider-2.1.0.tgz#1caeed18761ba2a390777418cc9389ba25e39bce" + integrity sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ== + dependencies: + "@chakra-ui/number-utils" "2.0.7" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-callback-ref" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-latest-ref" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-pan-event" "2.1.0" + "@chakra-ui/react-use-size" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + +"@chakra-ui/spinner@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/spinner/-/spinner-2.1.0.tgz#aa24a3d692c6ac90714e0f0f82c76c12c78c8e60" + integrity sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g== + dependencies: + "@chakra-ui/shared-utils" "2.0.5" -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= +"@chakra-ui/stat@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/stat/-/stat-2.1.1.tgz#a204ba915795345996a16c79794d84826d7dcc2d" + integrity sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q== + dependencies: + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -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== +"@chakra-ui/stepper@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/stepper/-/stepper-2.3.1.tgz#a0a0b73e147f202ab4e51cae55dad45489cc89fd" + integrity sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q== dependencies: - color-convert "^1.9.0" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -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== +"@chakra-ui/styled-system@2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz#898ab63da560a4a014f7b05fa7767e8c76da6d2f" + integrity sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg== dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" + "@chakra-ui/shared-utils" "2.0.5" + csstype "^3.1.2" + lodash.mergewith "4.6.2" -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== +"@chakra-ui/switch@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/switch/-/switch-2.1.2.tgz#f7c6878d8126bfac8fa3b939079f1017c21b7479" + integrity sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA== + dependencies: + "@chakra-ui/checkbox" "2.3.2" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/system@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/system/-/system-2.6.2.tgz#528ec955bd6a7f74da46470ee8225b1e2c80a78b" + integrity sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ== + dependencies: + "@chakra-ui/color-mode" "2.2.0" + "@chakra-ui/object-utils" "2.1.0" + "@chakra-ui/react-utils" "2.0.12" + "@chakra-ui/styled-system" "2.9.2" + "@chakra-ui/theme-utils" "2.0.21" + "@chakra-ui/utils" "2.0.15" + react-fast-compare "3.2.2" + +"@chakra-ui/table@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/table/-/table-2.1.0.tgz#20dce14c5e4d70dc7c6c0e87cce9b05907ff8c50" + integrity sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ== dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" -anymatch@~3.1.1: +"@chakra-ui/tabs@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/tabs/-/tabs-3.0.0.tgz#854c06880af26158d7c72881c4b5e0453f6c485d" + integrity sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw== + dependencies: + "@chakra-ui/clickable" "2.1.0" + "@chakra-ui/descendant" "3.1.0" + "@chakra-ui/lazy-utils" "2.0.5" + "@chakra-ui/react-children-utils" "2.0.6" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-controllable-state" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/react-use-safe-layout-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/tag@3.1.1": version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + resolved "https://registry.yarnpkg.com/@chakra-ui/tag/-/tag-3.1.1.tgz#d05284b6549a84d3a08e57eec57df3ad0eebd882" + integrity sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ== dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" + "@chakra-ui/icon" "3.2.0" + "@chakra-ui/react-context" "2.1.0" -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +"@chakra-ui/textarea@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/textarea/-/textarea-2.1.2.tgz#30f8af0e233cec2dee79d527450c6586e7122eff" + integrity sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA== + dependencies: + "@chakra-ui/form-control" "2.2.0" + "@chakra-ui/shared-utils" "2.0.5" -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== +"@chakra-ui/theme-tools@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/theme-tools/-/theme-tools-2.1.2.tgz#913be05879cd816c546993ccb9ff7615f85ff69f" + integrity sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA== + dependencies: + "@chakra-ui/anatomy" "2.2.2" + "@chakra-ui/shared-utils" "2.0.5" + color2k "^2.0.2" + +"@chakra-ui/theme-utils@2.0.21": + version "2.0.21" + resolved "https://registry.yarnpkg.com/@chakra-ui/theme-utils/-/theme-utils-2.0.21.tgz#da7ed541a5241a8ed0384eb14f37fa9b998382cf" + integrity sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw== + dependencies: + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/styled-system" "2.9.2" + "@chakra-ui/theme" "3.3.1" + lodash.mergewith "4.6.2" + +"@chakra-ui/theme@3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/theme/-/theme-3.3.1.tgz#75c6cd0b5c70c0aa955068274ee4780f299bd8a4" + integrity sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ== + dependencies: + "@chakra-ui/anatomy" "2.2.2" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/theme-tools" "2.1.2" + +"@chakra-ui/toast@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@chakra-ui/toast/-/toast-7.0.2.tgz#d1c396bbfced12e22b010899731fd8cc294d53ec" + integrity sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA== + dependencies: + "@chakra-ui/alert" "2.2.2" + "@chakra-ui/close-button" "2.1.1" + "@chakra-ui/portal" "2.1.0" + "@chakra-ui/react-context" "2.1.0" + "@chakra-ui/react-use-timeout" "2.1.0" + "@chakra-ui/react-use-update-effect" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + "@chakra-ui/styled-system" "2.9.2" + "@chakra-ui/theme" "3.3.1" + +"@chakra-ui/tooltip@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@chakra-ui/tooltip/-/tooltip-2.3.1.tgz#29fb8508a37bb6b20ab8dbb32bca6cd59b098796" + integrity sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A== + dependencies: + "@chakra-ui/dom-utils" "2.1.0" + "@chakra-ui/popper" "3.1.0" + "@chakra-ui/portal" "2.1.0" + "@chakra-ui/react-types" "2.0.7" + "@chakra-ui/react-use-disclosure" "2.1.0" + "@chakra-ui/react-use-event-listener" "2.1.0" + "@chakra-ui/react-use-merge-refs" "2.1.0" + "@chakra-ui/shared-utils" "2.0.5" + +"@chakra-ui/transition@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/transition/-/transition-2.1.0.tgz#c8e95564f7ab356e78119780037bae5ad150c7b3" + integrity sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ== dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" + "@chakra-ui/shared-utils" "2.0.5" -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== +"@chakra-ui/utils@2.0.15": + version "2.0.15" + resolved "https://registry.yarnpkg.com/@chakra-ui/utils/-/utils-2.0.15.tgz#bd800b1cff30eb5a5e8c36fa039f49984b4c5e4a" + integrity sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA== dependencies: - sprintf-js "~1.0.2" + "@types/lodash.mergewith" "4.6.7" + css-box-model "1.2.1" + framesync "6.1.2" + lodash.mergewith "4.6.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= +"@chakra-ui/visually-hidden@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@chakra-ui/visually-hidden/-/visually-hidden-2.2.0.tgz#9b0ecef8f01263ab808ba3bda7b36a0d91b4d5c1" + integrity sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ== + +"@emotion/babel-plugin@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" + integrity sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/runtime" "^7.18.3" + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/serialize" "^1.1.2" + babel-plugin-macros "^3.1.0" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.2.0" -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== +"@emotion/cache@^11.11.0", "@emotion/cache@^11.4.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" -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= +"@emotion/hash@^0.9.1": + version "0.9.1" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" + integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= +"@emotion/is-prop-valid@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + "@emotion/memoize" "^0.8.1" -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/react@^11.11.4", "@emotion/react@^11.8.1": + version "11.11.4" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d" + integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/cache" "^11.11.0" + "@emotion/serialize" "^1.1.3" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3", "@emotion/serialize@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451" + integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ== + dependencies: + "@emotion/hash" "^0.9.1" + "@emotion/memoize" "^0.8.1" + "@emotion/unitless" "^0.8.1" + "@emotion/utils" "^1.2.1" + csstype "^3.0.2" -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= +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + +"@emotion/styled@^11.11.5": + version "11.11.5" + resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.11.5.tgz#0c5c8febef9d86e8a926e663b2e5488705545dfb" + integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ== + dependencies: + "@babel/runtime" "^7.18.3" + "@emotion/babel-plugin" "^11.11.0" + "@emotion/is-prop-valid" "^1.2.2" + "@emotion/serialize" "^1.1.4" + "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" + "@emotion/utils" "^1.2.1" + +"@emotion/unitless@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== -arrify@^1.0.1: +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" + integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== -asn1.js@^4.0.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@floating-ui/core@^1.0.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.1.tgz#a4e6fef1b069cda533cbc7a4998c083a37f37573" + integrity sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A== dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" + "@floating-ui/utils" "^0.2.0" -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== +"@floating-ui/dom@^1.0.1": + version "1.6.5" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" + integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw== dependencies: - safer-buffer "~2.1.0" + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +"@floating-ui/utils@^0.2.0": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" + integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" +"@formatjs/cli@^6.2.10": + version "6.2.10" + resolved "https://registry.yarnpkg.com/@formatjs/cli/-/cli-6.2.10.tgz#5cb04bed018094c1ef4bbdf53fb783d63e7aba44" + integrity sha512-JdFw5oqPNB4qahie2Mcg6yXxyUJouN60OaEaejjMtGsgoPbFjS7X/9/Nj5vW+rAUTi0UNbOW+JPW9XNx6EuoHw== -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= +"@formatjs/ecma402-abstract@1.18.2": + version "1.18.2" + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.2.tgz#bf103712a406874eb1e387858d5be2371ab3aa14" + integrity sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA== + dependencies: + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" -ast-types@0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" - integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= +"@formatjs/fast-memoize@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.2.0.tgz#33bd616d2e486c3e8ef4e68c99648c196887802b" + integrity sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA== + dependencies: + tslib "^2.4.0" -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== +"@formatjs/icu-messageformat-parser@2.7.6": + version "2.7.6" + resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.7.6.tgz#3d69806de056d2919d53dad895a5ff4851e4e9ff" + integrity sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/icu-skeleton-parser" "1.8.0" + tslib "^2.4.0" -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== +"@formatjs/icu-skeleton-parser@1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.0.tgz#5f3d3a620c687d6f8c180d80d1241e8f213acf79" + integrity sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + tslib "^2.4.0" + +"@formatjs/intl-displaynames@6.6.6": + version "6.6.6" + resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.6.6.tgz#be9fea4d24f577bb1a9d0f3ef4f2dcdabb4fe42d" + integrity sha512-Dg5URSjx0uzF8VZXtHb6KYZ6LFEEhCbAbKoYChYHEOnMFTw/ZU3jIo/NrujzQD2EfKPgQzIq73LOUvW6Z/LpFA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/intl-listformat@7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@formatjs/intl-listformat/-/intl-listformat-7.5.5.tgz#e4c7d741f2201c65e7da71326726e61332c7161e" + integrity sha512-XoI52qrU6aBGJC9KJddqnacuBbPlb/bXFN+lIFVFhQ1RnFHpzuFrlFdjD9am2O7ZSYsyqzYRpkVcXeT1GHkwDQ== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/intl-localematcher" "0.5.4" + tslib "^2.4.0" + +"@formatjs/intl-localematcher@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz#caa71f2e40d93e37d58be35cfffe57865f2b366f" + integrity sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g== + dependencies: + tslib "^2.4.0" + +"@formatjs/intl@2.10.2": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.10.2.tgz#c074439ac2dbde4c2b3768b8108dfc3932b7fb30" + integrity sha512-raPGWr3JRv3neXV78SqPFrGC05fIbhhNzVghHNxFde27ls2KkXiMhtP7HBybjGpikVSjjhdhaZto+4p1vmm9bQ== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.7.6" + "@formatjs/intl-displaynames" "6.6.6" + "@formatjs/intl-listformat" "7.5.5" + intl-messageformat "10.5.12" + tslib "^2.4.0" + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -async-foreach@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" - integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -async@~0.2.6: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -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== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -aws4@^1.8.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" - integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@^6.26.0, babel-core@^6.26.3: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" -babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" - integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ= - dependencies: - babel-helper-explode-assignable-expression "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-call-delegate@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" - integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-define-map@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" - integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-explode-assignable-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" - integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo= - dependencies: - babel-runtime "^6.22.0" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-function-name@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" - integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk= - dependencies: - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-get-function-arity@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" - integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-hoist-variables@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" - integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-optimise-call-expression@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" - integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-helper-regex@^6.24.1: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" - integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI= - dependencies: - babel-runtime "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-helper-remap-async-to-generator@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" - integrity sha1-XsWBgnrXI/7N04HxySg5BnbkVRs= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helper-replace-supers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" - integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo= - dependencies: - babel-helper-optimise-call-expression "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-loader@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" - integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== - dependencies: - find-cache-dir "^2.1.0" - loader-utils "^1.4.0" - mkdirp "^0.5.3" - pify "^4.0.1" - schema-utils "^2.6.5" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-check-es2015-constants@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" - integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-syntax-async-functions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" - integrity sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU= - -babel-plugin-syntax-exponentiation-operator@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" - integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4= - -babel-plugin-syntax-trailing-function-commas@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" - integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM= - -babel-plugin-transform-async-to-generator@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" - integrity sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E= - dependencies: - babel-helper-remap-async-to-generator "^6.24.1" - babel-plugin-syntax-async-functions "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-arrow-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" - integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" - integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-block-scoping@^6.23.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" - integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8= - dependencies: - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - lodash "^4.17.4" - -babel-plugin-transform-es2015-classes@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" - integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs= - dependencies: - babel-helper-define-map "^6.24.1" - babel-helper-function-name "^6.24.1" - babel-helper-optimise-call-expression "^6.24.1" - babel-helper-replace-supers "^6.24.1" - babel-messages "^6.23.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-computed-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" - integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-destructuring@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" - integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0= +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: - babel-runtime "^6.22.0" + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" -babel-plugin-transform-es2015-duplicate-keys@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" - integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4= +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + +"@popperjs/core@^2.9.3": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + +"@remix-run/router@1.16.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.16.0.tgz#0e10181e5fec1434eb071a9bc4bdaac843f16dcc" + integrity sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q== + +"@rollup/rollup-android-arm-eabi@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d" + integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ== + +"@rollup/rollup-android-arm64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b" + integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw== + +"@rollup/rollup-darwin-arm64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz#6b66aaf003c70454c292cd5f0236ebdc6ffbdf1a" + integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw== + +"@rollup/rollup-darwin-x64@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b" + integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14" + integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A== + +"@rollup/rollup-linux-arm-musleabihf@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb" + integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg== + +"@rollup/rollup-linux-arm64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a" + integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A== + +"@rollup/rollup-linux-arm64-musl@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af" + integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571" + integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ== + +"@rollup/rollup-linux-riscv64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f" + integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg== + +"@rollup/rollup-linux-s390x-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354" + integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g== + +"@rollup/rollup-linux-x64-gnu@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811" + integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ== + +"@rollup/rollup-linux-x64-musl@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385" + integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q== + +"@rollup/rollup-win32-arm64-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f" + integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA== + +"@rollup/rollup-win32-ia32-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411" + integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ== + +"@rollup/rollup-win32-x64-msvc@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503" + integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w== + +"@tanstack/query-core@5.35.1": + version "5.35.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.35.1.tgz#a7e6cbf0f252475da19ba9cba6567e8fe683b2ee" + integrity sha512-0Dnpybqb8+ps6WgqBnqFEC+1F/xLvUosRAq+wiGisTgolOZzqZfkE2995dEXmhuzINiTM7/a6xSGznU0NIvBkw== + +"@tanstack/query-devtools@5.32.1": + version "5.32.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.32.1.tgz#2c03f2fbe9162b650e697c469c8618c7a05d593f" + integrity sha512-7Xq57Ctopiy/4atpb0uNY5VRuCqRS/1fi/WBCKKX6jHMa6cCgDuV/AQuiwRXcKARbq2OkVAOrW2v4xK9nTbcCA== + +"@tanstack/react-query-devtools@^5.35.1": + version "5.35.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.35.1.tgz#070109a9a64e2d74e9662a2cac91dc364d88c04a" + integrity sha512-G2TP8ekCo+C9IPdEswKB9mqG5pxV+DWq86lmNw/VbUpdyNwNFvKi7GdcqW1pLDi5al+zifSjGSO7QZ7zDMJcQg== + dependencies: + "@tanstack/query-devtools" "5.32.1" + +"@tanstack/react-query@^5.35.1": + version "5.35.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.35.1.tgz#3b7c710bb32c6c71e0cbe64029dded9b0cbd9a0f" + integrity sha512-i2T7m2ffQdNqlX3pO+uMsnQ0H4a59Ens2GxtlMsRiOvdSB4SfYmHb27MnvFV8rGmtWRaa4gPli0/rpDoSS5LbQ== + dependencies: + "@tanstack/query-core" "5.35.1" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" + integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== + dependencies: + "@babel/types" "^7.20.7" + +"@types/country-flag-icons@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/country-flag-icons/-/country-flag-icons-1.2.2.tgz#8f51089cab857f0f700feabd38b3960d006d64f2" + integrity sha512-CefEn/J336TBDp7NX8JqzlDtCBOsm8M3r1Li0gEOt0HOMHF1XemNyrx9lSHjsafcb1yYWybU0N8ZAXuyCaND0w== + +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" + "@types/ms" "*" -babel-plugin-transform-es2015-for-of@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" - integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-function-name@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" - integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos= - dependencies: - babel-helper-function-name "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" - integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" - integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ= - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: - version "6.26.2" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-es2015-modules-systemjs@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" - integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM= - dependencies: - babel-helper-hoist-variables "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-modules-umd@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" - integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg= - dependencies: - babel-plugin-transform-es2015-modules-amd "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-plugin-transform-es2015-object-super@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" - integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40= - dependencies: - babel-helper-replace-supers "^6.24.1" - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-parameters@^6.23.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" - integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys= - dependencies: - babel-helper-call-delegate "^6.24.1" - babel-helper-get-function-arity "^6.24.1" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-traverse "^6.24.1" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-shorthand-properties@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" - integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-spread@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" - integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-sticky-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" - integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-plugin-transform-es2015-template-literals@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" - integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-typeof-symbol@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" - integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-transform-es2015-unicode-regex@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" - integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek= - dependencies: - babel-helper-regex "^6.24.1" - babel-runtime "^6.22.0" - regexpu-core "^2.0.0" - -babel-plugin-transform-exponentiation-operator@^6.22.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" - integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4= - dependencies: - babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" - babel-plugin-syntax-exponentiation-operator "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-regenerator@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" - integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8= - dependencies: - regenerator-transform "^0.10.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-env@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" - integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg== - dependencies: - babel-plugin-check-es2015-constants "^6.22.0" - babel-plugin-syntax-trailing-function-commas "^6.22.0" - babel-plugin-transform-async-to-generator "^6.22.0" - babel-plugin-transform-es2015-arrow-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" - babel-plugin-transform-es2015-block-scoping "^6.23.0" - babel-plugin-transform-es2015-classes "^6.23.0" - babel-plugin-transform-es2015-computed-properties "^6.22.0" - babel-plugin-transform-es2015-destructuring "^6.23.0" - babel-plugin-transform-es2015-duplicate-keys "^6.22.0" - babel-plugin-transform-es2015-for-of "^6.23.0" - babel-plugin-transform-es2015-function-name "^6.22.0" - babel-plugin-transform-es2015-literals "^6.22.0" - babel-plugin-transform-es2015-modules-amd "^6.22.0" - babel-plugin-transform-es2015-modules-commonjs "^6.23.0" - babel-plugin-transform-es2015-modules-systemjs "^6.23.0" - babel-plugin-transform-es2015-modules-umd "^6.23.0" - babel-plugin-transform-es2015-object-super "^6.22.0" - babel-plugin-transform-es2015-parameters "^6.23.0" - babel-plugin-transform-es2015-shorthand-properties "^6.22.0" - babel-plugin-transform-es2015-spread "^6.22.0" - babel-plugin-transform-es2015-sticky-regex "^6.22.0" - babel-plugin-transform-es2015-template-literals "^6.22.0" - babel-plugin-transform-es2015-typeof-symbol "^6.23.0" - babel-plugin-transform-es2015-unicode-regex "^6.22.0" - babel-plugin-transform-exponentiation-operator "^6.22.0" - babel-plugin-transform-regenerator "^6.22.0" - browserslist "^3.2.6" - invariant "^2.2.2" - semver "^5.3.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.24.1, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== +"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"backbone.marionette@^4.0.0, 4.0.0-beta.1": - version "4.0.0-beta.1" - resolved "https://registry.yarnpkg.com/backbone.marionette/-/backbone.marionette-4.0.0-beta.1.tgz#793528fc5c60620200e2440e6dcd04ba16c3aab3" - integrity sha1-eTUo/FxgYgIA4kQObc0EuhbDqrM= +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== dependencies: - backbone.radio "^2.0.0" + "@types/unist" "^2" -backbone.marionette@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/backbone.marionette/-/backbone.marionette-4.1.2.tgz#55de74363219f6d5c343dab5bff6aeb20fc44419" - integrity sha512-T8wWxZZnuYjylONTnWZsGsgXtdx2ZrE38pZWJI9LmPqzYK5j0T8uduapFO7OEpsW5rtdbBgwof30xhzAkbb5eQ== +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== dependencies: - backbone.radio "^2.0.0" + "@types/unist" "*" -backbone.radio@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/backbone.radio/-/backbone.radio-2.0.0.tgz#bbe8672b373e313f99f36d2fbcf583fe77d04f42" - integrity sha1-u+hnKzc+MT+Z820vvPWD/nfQT0I= +"@types/history@^4.7.11": + version "4.7.11" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" + integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== -backbone@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/backbone/-/backbone-1.4.0.tgz#54db4de9df7c3811c3f032f34749a4cd27f3bd12" - integrity sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ== +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.1": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: - underscore ">=1.8.3" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" -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= +"@types/humps@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/humps/-/humps-2.0.6.tgz#a358688fe092e40b5f50261e0a55e2fa6d68cabe" + integrity sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg== -base62@^1.1.0: - version "1.2.8" - resolved "https://registry.yarnpkg.com/base62/-/base62-1.2.8.tgz#1264cb0fb848d875792877479dbe8bae6bae3428" - integrity sha512-V6YHUbjLxN1ymqNLb1DPHoU1CpfdL7d2YTIp5W3U4hhoG4hhxNmsFDs66M9EXxBiSEke5Bt5dwdfMwwZF70iLA== - -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -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" - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/lodash.mergewith@4.6.7": + version "4.6.7" + resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz#eaa65aa5872abdd282f271eae447b115b2757212" + integrity sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A== dependencies: - tweetnacl "^0.14.3" + "@types/lodash" "*" -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== +"@types/lodash@*", "@types/lodash@4.17.1": + version "4.17.1" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8" + integrity sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q== -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +"@types/mdast@^4.0.0": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.3.tgz#1e011ff013566e919a4232d1701ad30d70cab333" + integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== + dependencies: + "@types/unist" "*" -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -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== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== +"@types/node@20.12.10": + version "20.12.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.10.tgz#8f0c3f12b0f075eee1fe20c1afb417e9765bef76" + integrity sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw== dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" - integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== + undici-types "~5.26.4" -boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== -bootstrap@^4.0.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.1.tgz#f7322c7dd3e6376d430efc0c3f57e4d8005eb5b2" - integrity sha512-bxUooHBSbvefnIZfjD0LE8nfdPKrtiFy2sgrxQwUZ0UpFzpjVbVMUxaGIoo9XWT4B2LG1HX6UQg0UMOakT0prQ== +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== -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" +"@types/react-dom@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" -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== +"@types/react-router-dom@5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" + integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router" "*" -braces@^2.3.1, braces@^2.3.2: - 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.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== +"@types/react-router@*": + version "5.1.20" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" + integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== dependencies: - fill-range "^7.0.1" + "@types/history" "^4.7.11" + "@types/react" "*" -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +"@types/react-syntax-highlighter@^15.5.13": + version "15.5.13" + resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz#c5baf62a3219b3bf28d39cfea55d0a49a263d1f2" + integrity sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA== + dependencies: + "@types/react" "*" -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== +"@types/react-table@^7.7.20": + version "7.7.20" + resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.7.20.tgz#2f68e70ca7a703ad8011a8da55c38482f0eb4314" + integrity sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w== dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" + "@types/react" "*" -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== +"@types/react-transition-group@^4.4.0": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" + "@types/react" "*" -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== +"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@18.3.1": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.1.tgz#fed43985caa834a2084d002e4771e15dfcbdbe8e" + integrity sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw== dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" + "@types/prop-types" "*" + csstype "^3.0.2" -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/styled-components@5.1.34": + version "5.1.34" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.34.tgz#4107df8ef8a7eaba4fa6b05f78f93fba4daf0300" + integrity sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA== dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" + "@types/hoist-non-react-statics" "*" + "@types/react" "*" + csstype "^3.0.2" + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" + integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== + +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" + integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== + +"@typescript-eslint/eslint-plugin@^6.1.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.1.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + dependencies: + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== + dependencies: + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -browserify-sign@^4.0.0: +"@vitejs/plugin-react@^4.2.1": version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz#744d8e4fcb120fc3dbaa471dadd3483f5a304bb9" + integrity sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ== + dependencies: + "@babel/core" "^7.23.5" + "@babel/plugin-transform-react-jsx-self" "^7.23.3" + "@babel/plugin-transform-react-jsx-source" "^7.23.3" + "@types/babel__core" "^7.20.5" + react-refresh "^0.14.0" + +"@zag-js/dom-query@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@zag-js/dom-query/-/dom-query-0.16.0.tgz#bca46bcd78f78c900064478646d95f9781ed098e" + integrity sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ== + +"@zag-js/element-size@0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@zag-js/element-size/-/element-size-0.10.5.tgz#a24bad2eeb7e2c8709e32be5336e158e1a1a174f" + integrity sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w== + +"@zag-js/focus-visible@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@zag-js/focus-visible/-/focus-visible-0.16.0.tgz#c9e53e3dbab0f2649d04a489bb379f5800f4f069" + integrity sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA== + dependencies: + "@zag-js/dom-query" "0.16.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: - pako "~1.0.5" + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" -browserslist@^3.2.6: - version "3.2.8" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" - integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ== +ajv@^8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" + integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== dependencies: - caniuse-lite "^1.0.30000844" - electron-to-chromium "^1.3.47" + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== +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: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" + color-convert "^1.9.0" -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -cacache@^12.0.2, cacache@^12.0.3: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -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== +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 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" + color-convert "^2.0.1" -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== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" -camel-case@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-hidden@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" + tslib "^2.0.0" -camel-case@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.1.tgz#1fc41c854f00e2f7d0139dfeba1542d6896fe547" - integrity sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q== +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== dependencies: - pascal-case "^3.1.1" - tslib "^1.10.0" + dequal "^2.0.3" -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" + call-bind "^1.0.5" + is-array-buffer "^3.0.4" -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" -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== +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -caniuse-lite@^1.0.30000844: - version "1.0.30001111" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001111.tgz#dd0ce822c70eb6c7c068e4a55c22e19ec1501298" - integrity sha512-xnDje2wchd/8mlJu8sXvWxOGvMgv+uT3iZ3bkIAynKOzToCssWCmkz/ZIkQBs/2pUB4uwnJKVORWQ31UkbVjOg== +array.prototype.findlast@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" -chalk@^1.1.1, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.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== +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.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== +array.prototype.tosorted@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" -chalk@^4.0.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== +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" + possible-typed-array-names "^1.0.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@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== -chokidar@^3.2.2, chokidar@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" - integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== 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" + dequal "^2.0.3" -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== +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== -chrome-trace-event@^1.0.2: +balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -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== +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== +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: - inherits "^2.0.1" - safe-buffer "^5.0.1" + balanced-match "^1.0.0" + concat-map "0.0.1" -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== +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" + balanced-match "^1.0.0" -clean-css@4.2.x, clean-css@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== +braces@^3.0.2, 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: - source-map "~0.6.0" + fill-range "^7.0.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== +browserslist@^4.22.2: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" -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== +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== dependencies: - restore-cursor "^3.1.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" -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== +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== -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" +caniuse-lite@^1.0.30001587: + version "1.0.30001616" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz#4342712750d35f71ebba9fcac65e2cf8870013c3" + integrity sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw== -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chakra-react-select@^4.7.6: + version "4.7.6" + resolved "https://registry.yarnpkg.com/chakra-react-select/-/chakra-react-select-4.7.6.tgz#3be8ffb314b8e75ef02663fd3e2fdf872b79683b" + integrity sha512-ZL43hyXPnWf1g/HjsZDecbeJ4F2Q6tTPYJozlKWkrQ7lIX7ORP0aZYwmc5/Wly4UNzMimj2Vuosl6MmIXH+G2g== dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" + react-select "5.7.7" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== +chalk@^2.4.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: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.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= +chalk@^4.0.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: - mimic-response "^1.0.0" + ansi-styles "^4.1.0" + supports-color "^7.1.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= +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== -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= +character-entities-legacy@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" + integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" + integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" + integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== color-convert@^1.9.0: version "1.9.3" @@ -1834,212 +2287,77 @@ color-convert@^2.0.1: 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= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== 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== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.17.x: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commander@^2.20.0, commander@^2.5.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +color2k@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/color2k/-/color2k-2.0.3.tgz#a771244f6b6285541c82aa65ff0a0c624046e533" + integrity sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog== -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== +comma-separated-tokens@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" + integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@~2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== -common-prefix@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/common-prefix/-/common-prefix-1.1.0.tgz#e3a5ea7fafaefc7eb84e760523e1afb985f90f00" - integrity sha1-46Xqf6+u/H64TnYFI+GvuYX5DwA= +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -commoner@^0.10.1: - version "0.10.8" - resolved "https://registry.yarnpkg.com/commoner/-/commoner-0.10.8.tgz#34fc3672cd24393e8bb47e70caa0293811f4f2c5" - integrity sha1-NPw2cs0kOT6LtH5wyqApOBH08sU= - dependencies: - commander "^2.5.0" - detective "^4.3.1" - glob "^5.0.15" - graceful-fs "^4.1.2" - iconv-lite "^0.4.5" - mkdirp "^0.5.0" - private "^0.1.6" - q "^1.1.2" - recast "^0.11.17" - -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== +compute-scroll-into-view@3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz#c418900a5c56e2b04b885b54995df164535962b1" + integrity sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A== 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= - -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -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-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -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= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= +convert-source-map@^1.5.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== -convert-source-map@^1.5.1, convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== +copy-to-clipboard@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -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= + toggle-selection "^1.0.6" -copy-webpack-plugin@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz#5481a03dea1123d88a988c6ff8b78247214f0b88" - integrity sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg== +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== dependencies: - cacache "^12.0.3" - find-cache-dir "^2.1.0" - glob-parent "^3.1.0" - globby "^7.1.1" - is-glob "^4.0.1" - loader-utils "^1.2.3" - minimatch "^3.0.4" - normalize-path "^3.0.0" - p-limit "^2.2.1" - schema-utils "^1.0.0" - serialize-javascript "^2.1.2" - webpack-log "^2.0.0" - -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= - -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.11" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" - integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== - -core-util-is@1.0.2, 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= + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" +country-flag-icons@^1.5.11: + version "1.5.11" + resolved "https://registry.yarnpkg.com/country-flag-icons/-/country-flag-icons-1.5.11.tgz#04c0556728e517a6207946656355698ac6237080" + integrity sha512-B+mvFywunkRJs270k7kCBjhogvIA0uNn6GAXv6m2cPn3rrwqZzZVr2gBWcz+Cz7OGVWlcbERlYRIX0S6OGr8Bw== -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -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" - -cross-spawn@^7.0.3: +cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2048,232 +2366,125 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -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== - -css-loader@^3.5.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" - integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== - dependencies: - camelcase "^5.3.1" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.32" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^2.7.0" - semver "^6.3.0" - -css-select@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= +css-box-model@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" + tiny-invariant "^1.0.6" -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +csstype@^3.0.2, csstype@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== -cyclist@^1.0.1: +data-view-buffer@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -d3@^3.5.6: - version "3.5.17" - resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" - integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== dependencies: - assert-plus "^1.0.0" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" -debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== dependencies: - ms "2.0.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.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== +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== dependencies: - ms "^2.1.1" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" -debug@^4.0.1, debug@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== +date-fns@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" + ms "2.1.2" -decamelize@^1.0.0, decamelize@^1.1.0, 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= +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== dependencies: - mimic-response "^1.0.0" + character-entities "^2.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== +decode-uri-component@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.4.1.tgz#2ac4859663c704be22bf7db760a1494a49ab2cc5" + integrity sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ== -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= +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -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-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -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" - -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -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= + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== dependencies: - repeating "^2.0.0" + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" -detective@^4.3.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e" - integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig== - dependencies: - acorn "^5.2.1" - defined "^1.0.0" +dequal@^2.0.0, dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== -dir-glob@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== dependencies: - path-type "^3.0.0" + dequal "^2.0.0" dir-glob@^3.0.1: version "3.0.1" @@ -2282,6 +2493,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -2289,252 +2507,145 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-converter@^0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa" - integrity sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA== +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: - no-case "^3.0.3" - tslib "^1.10.0" + "@babel/runtime" "^7.8.7" + csstype "^3.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" +electron-to-chromium@^1.4.668: + version "1.4.759" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.759.tgz#ed7f4d9eed25124708ab0f4422e66b16534648d3" + integrity sha512-qZJc+zsuI+/5UjOSFnpkJBwwLMH1AZgyKqJ7LUNnRsB7v/cDjMu9DvXgp9kH6PTTZxjnPXGp2Uhurw+2Ll4Hjg== -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== +enhanced-resolve@^5.12.0: + version "5.16.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz#e8bc63d51b826d6f1cbc0a150ecb5a8b0c62e567" + integrity sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw== dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" + graceful-fs "^4.2.4" + tapable "^2.2.0" -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= +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: - jsbn "~0.1.0" - safer-buffer "^2.1.0" + is-arrayish "^0.2.1" -ejs-include-regex@^1.0.0: +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/ejs-include-regex/-/ejs-include-regex-1.0.0.tgz#e2f71575cbfd551ac800b2474c1f62a38e70093a" - integrity sha1-4vcVdcv9VRrIALJHTB9io45wCTo= - -ejs-lint@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ejs-lint/-/ejs-lint-1.1.0.tgz#f3fd7b164fde618bcefb8e5ece8f9ac206739ce0" - integrity sha512-SnOxzUtJug5C92wtFTZ+Zyb+eoqOlN+WyqDk6HDSXgY5FseDgm8MhZ/r70nkzrh0AMPpifzebep3gGILzAlQNg== - dependencies: - chalk "^4.0.0" - ejs "3.0.1" - ejs-include-regex "^1.0.0" - globby "^11.0.0" - read-input "^0.3.1" - rewire "^5.0.0" - syntax-error "^1.1.6" - yargs "^15.0.0" - -ejs-loader@^0.3.6: - version "0.3.7" - resolved "https://registry.yarnpkg.com/ejs-loader/-/ejs-loader-0.3.7.tgz#82d3cd0a3d3f64d519332b95f9b8a7897c9fcaf4" - integrity sha512-K1HBDWXQZkcIAnP5h65kWsD7o7NABvHswOH49rVHX7POGaTM2kYQfkFZVn4ZQeiRnzqbtf07LxSitOVRdR98GA== - dependencies: - loader-utils "^0.2.7" - lodash "^4.17.15" - -ejs-webpack-loader@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/ejs-webpack-loader/-/ejs-webpack-loader-2.2.2.tgz#0536acdd79ba4cdbefb4248fcbe7441e264955d7" - integrity sha512-fuZ5djtVnvoMv4xlyQs3sh9JfIh167iPg7Q1ABFdQIbPHqRgeRWQCvodGybQhiRRCUIeqH9HPtfB8hJimPSPbA== - dependencies: - ejs "^2.0.0" - html-minifier "^3" - loader-utils "^0.2.7" - merge "^1.2.0" - uglify-js "~2.6.1" - -ejs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.0.1.tgz#30c8f6ee9948502cc32e85c37a3f8b39b5a614a5" - integrity sha512-cuIMtJwxvzumSAkqaaoGY/L6Fc/t6YvoP9/VIaK0V/CyqKLEQ8sqODmYfy/cjXEdZ9+OOL8TecbJu+1RsofGDw== - -ejs@^2.0.0: - version "2.7.4" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" - integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== - -electron-to-chromium@^1.3.47: - version "1.3.522" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.522.tgz#4a6485ad187ffd31913bba0747d0e36405f405d6" - integrity sha512-67V62Z4CFOiAtox+o+tosGfVk0QX4DJgH609tjT8QymbJZVAI/jWnAthnr8c5hnRNziIRwkc9EMQYejiVz3/9Q== - -elliptic@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -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== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -end-of-stream@^1.0.0, 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== + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: - once "^1.4.0" + get-intrinsic "^1.2.4" -enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" - integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== +es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" + es-errors "^1.3.0" -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: +es-set-tostringtag@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -envify@^3.0.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/envify/-/envify-3.4.1.tgz#d7122329e8df1688ba771b12501917c9ce5cbce8" - integrity sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg= - dependencies: - jstransform "^11.0.3" - through "~2.3.4" - -errno@^0.1.3, errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -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== + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== dependencies: - is-arrayish "^0.2.1" + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + hasown "^2.0.0" es-to-primitive@^1.2.1: version "1.2.1" @@ -2545,312 +2656,322 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -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-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +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-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -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== + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -eslint@^6.8.0: +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.8.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.5.5: + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.27.5: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.7.1: 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" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + dependencies: + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + +eslint-plugin-prettier@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" + +eslint-plugin-react-hooks@^4.6.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react-refresh@^0.4.3: + version "0.4.6" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz#e8e8accab681861baed00c5c12da70267db0936f" + integrity sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA== + +eslint-plugin-react@^7.32.2: + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.17" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.10" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.45.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" 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" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" 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" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" - inquirer "^7.0.0" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" 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" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -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== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^7.1.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" -esprima-fb@^15001.1.0-dev-harmony-fb: - version "15001.1.0-dev-harmony-fb" - resolved "https://registry.yarnpkg.com/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz#30a947303c6b8d5e955bee2b99b1d233206a6901" - integrity sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE= - -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== - -esprima@~3.1.0: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -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== +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== 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== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.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, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -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== +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== 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== -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -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" - -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.2: +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" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: 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-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" 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: +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= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" -fbjs@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7" - integrity sha1-lja3cF9bqWhNRLcveDISVK/IYPc= - dependencies: - core-js "^1.0.0" - loose-envify "^1.0.0" - promise "^7.0.3" - ua-parser-js "^0.7.9" - whatwg-fetch "^0.9.0" - -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -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" - -file-loader@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.0.0.tgz#97bbfaab7a2460c07bcbd72d3a6922407f67649f" - integrity sha512-/aMOAYEFXDdjG0wytpTL5YQLfZnnTmLNjn+AIrJ/6HVnTfDqLsVKUUwkDf4I4kgex36BvjuXEn/TX9B/1ESyqQ== +fault@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" + integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA== dependencies: - loader-utils "^2.0.0" - schema-utils "^2.6.5" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + format "^0.2.0" -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= +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" + flat-cache "^3.0.4" fill-range@^7.0.1: version "7.0.1" @@ -2859,756 +2980,467 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" +filter-obj@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed" + integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -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== +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^5.0.0" + locate-path "^6.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" - -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== - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -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= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" -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" +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= +focus-lock@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-1.3.5.tgz#aa644576e5ec47d227b57eb14e1efb2abf33914c" + integrity sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ== dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" + tslib "^2.0.3" -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== dependencies: - minipass "^3.0.0" + is-callable "^1.1.3" -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +formik@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686" + integrity sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g== + dependencies: + "@types/hoist-non-react-statics" "^3.3.1" + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.21" + lodash-es "^4.17.21" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^2.0.0" + +framer-motion@^11.1.9: + version "11.1.9" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.1.9.tgz#1ef021fc35615eb83d6baa903a47ba872be99187" + integrity sha512-flECDIPV4QDNcOrDafVFiIazp8X01HFpzc01eDKJsdNH/wrATcYydJSH9JbPWMS8UD5lZlw+J1sK8LG2kICgqw== + dependencies: + tslib "^2.4.0" + +framesync@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.1.2.tgz#755eff2fb5b8f3b4d2b266dd18121b300aefea27" + integrity sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g== + dependencies: + tslib "2.4.0" + +fs-extra@^11.1.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.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@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -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" - -gaze@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== dependencies: - globule "^1.0.0" - -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" -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-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== -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" +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -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== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - pump "^3.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.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= +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== dependencies: - assert-plus "^1.0.0" + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= +get-tsconfig@^4.5.0: + version "4.7.4" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.4.tgz#228e1a3e37125aeb4467e9b992b92c4533093bd2" + integrity sha512-ofbkKj+0pjXjhejr007J/fLf+sW+8H7K5GCm+msC8q3IpvgjobpyPqSRFemNyIMxklC0zeJpi7VDFna19FacvQ== dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" + resolve-pkg-maps "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.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== +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" + is-glob "^4.0.3" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@~7.1.1: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" 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" +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -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== +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" + type-fest "^0.20.2" -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== dependencies: - global-prefix "^3.0.0" + define-properties "^1.2.1" + gopd "^1.0.1" -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= +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" + get-intrinsic "^1.1.3" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -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" - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globby@^11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" - integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA= - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - -globule@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" - integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - -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.11, 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== - -graceful-fs@^4.2.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== 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= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== 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-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -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= +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== 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== + es-define-property "^1.0.0" -has@^1.0.3: +has-proto@^1.0.1, has-proto@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@1.2.x, he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -homedir-polyfill@^1.0.1: +has-symbols@^1.0.2, has-symbols@^1.0.3: 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" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: - lru-cache "^6.0.0" - -html-minifier-terser@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" - integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== - dependencies: - camel-case "^4.1.1" - clean-css "^4.2.3" - commander "^4.1.1" - he "^1.2.0" - param-case "^3.0.3" - relateurl "^0.2.7" - terser "^4.6.3" - -html-minifier@^3: - version "3.5.21" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" - integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== - dependencies: - camel-case "3.0.x" - clean-css "4.2.x" - commander "2.17.x" - he "1.2.x" - param-case "2.1.x" - relateurl "0.2.x" - uglify-js "3.4.x" - -html-webpack-plugin@^4.0.4: - version "4.3.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd" - integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w== - dependencies: - "@types/html-minifier-terser" "^5.0.0" - "@types/tapable" "^1.0.5" - "@types/webpack" "^4.41.8" - html-minifier-terser "^5.0.1" - loader-utils "^1.2.3" - lodash "^4.17.15" - pretty-error "^2.1.1" - tapable "^1.1.3" - util.promisify "1.0.0" - -htmlparser2@^3.3.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -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== + has-symbols "^1.0.3" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + function-bind "^1.1.2" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= +hast-util-parse-selector@^2.0.0: + version "2.2.5" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" + integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== -iconv-lite@^0.4.24, iconv-lite@^0.4.5: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz#3ed27caf8dc175080117706bf7269404a0aa4f7c" + integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== dependencies: - safer-buffer ">= 2.1.2 < 3" + "@types/hast" "^3.0.0" -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== +hastscript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" + integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== dependencies: - postcss "^7.0.14" + "@types/hast" "^2.0.0" + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.0.0" + property-information "^5.0.0" + space-separated-tokens "^1.0.0" -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +highlight.js@^10.4.1, highlight.js@~10.7.0: + version "10.7.3" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" + integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" -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= +html-url-attributes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.0.tgz#fc4abf0c3fb437e2329c678b80abb3c62cff6f08" + integrity sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow== -ignore@^3.3.5: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +humps@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" + integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g== -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== +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== +immutable@^4.0.0: + version "4.3.5" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" + integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== -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== +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 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= - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imports-loader@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.8.0.tgz#030ea51b8ca05977c40a3abfd9b4088fe0be9a69" - integrity sha512-kXWL7Scp8KQ4552ZcdVTeaQCZSLW+e6nJfp3cwUMB673T7Hr98Xjx5JK+ql7ADlJUvj1JS5O01RLbKoutN5QDQ== - dependencies: - loader-utils "^1.0.2" - source-map "^0.6.1" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -infer-owner@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2: 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.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= +inline-style-parser@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" + integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== -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" +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" -interpret@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +intl-messageformat@10.5.12: + version "10.5.12" + resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.5.12.tgz#a0c1a20da896b7a1f4ba1b59c8ba5d9943c29c3f" + integrity sha512-izl0uxhy/melhw8gP2r8pGiVieviZmM4v5Oqx3c1/R7g9cwER2smmGfSjcIsp8Y3Q53bfciL/gkxacJRx/dUvg== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/fast-memoize" "2.2.0" + "@formatjs/icu-messageformat-parser" "2.7.6" + tslib "^2.4.0" -invariant@^2.2.2: +invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" -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= +is-alphabetical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" + integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" + integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== dependencies: - kind-of "^3.0.2" + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" -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== +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== dependencies: - kind-of "^6.0.0" + call-bind "^1.0.2" + get-intrinsic "^1.2.1" 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= + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== dependencies: - binary-extensions "^1.0.0" + has-bigints "^1.0.1" is-binary-path@~2.1.0: version "2.1.0" @@ -3617,295 +3449,221 @@ is-binary-path@~2.1.0: 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-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - -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-core-module@^2.5.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.7.0.tgz#3c0ef7d31b4acfc574f80c58409d568a836848e3" - integrity sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ== +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: - has "^1.0.3" + call-bind "^1.0.2" + has-tostringtag "^1.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-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -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== +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + hasown "^2.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== +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" + is-typed-array "^1.1.13" -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== +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" + has-tostringtag "^1.0.0" -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-decimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" + integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== -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-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== -is-extglob@^2.1.0, is-extglob@^2.1.1: +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= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -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= +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== 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== + call-bind "^1.0.2" -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== dependencies: - is-extglob "^2.1.0" + has-tostringtag "^1.0.0" -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== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 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-hexadecimal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" + integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== -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-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== -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= +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== dependencies: - kind-of "^3.0.2" + has-tostringtag "^1.0.0" 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.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -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-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== -is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -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== +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== dependencies: - isobject "^3.0.1" + call-bind "^1.0.7" -is-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== dependencies: - has-symbols "^1.0.1" + has-tostringtag "^1.0.0" -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== dependencies: - has-symbols "^1.0.1" + has-symbols "^1.0.2" -is-typedarray@^1.0.0, 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-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" -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-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" -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== +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" -isarray@1.0.0, 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= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== 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= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -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= +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== 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= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jquery-mask-plugin@^1.14.16: - version "1.14.16" - resolved "https://registry.yarnpkg.com/jquery-mask-plugin/-/jquery-mask-plugin-1.14.16.tgz#9ebb55947d984da5aade45315b2fe6b113e28aae" - integrity sha512-reywdHlYEkPbzWjTpcc1fk9XQ3PLvO5dzEAVqy8zI7NTF22tB1HbeU3iboZTLdkBEPaWAqeI2HtEjsGQ4roZKw== - -jquery-serializejson@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/jquery-serializejson/-/jquery-serializejson-2.9.0.tgz#03e3764e3a4b42c1c5aae9f93d7f19320c5f35a6" - integrity sha512-xR7rjl0tRKIVioV5lOkOSv7K8BHMvGzYzC7Ech1iAYuZiYf0ksEpLC8OqjA5VApXf/qn/49O9hTmW70+/EA0vA== - -jquery@^3.5.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" - integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== - -js-base64@^2.1.8: - version "2.6.4" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" - integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" "js-tokens@^3.0.0 || ^4.0.0", 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-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -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== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + argparse "^2.0.1" jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -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.2: - 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-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0: version "2.3.1" @@ -3917,208 +3675,130 @@ json-schema-traverse@^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-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== 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= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json5@^0.5.0, json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" -json5@^2.1.2: - 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" +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jstransform@^11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/jstransform/-/jstransform-11.0.3.tgz#09a78993e0ae4d4ef4487f6155a91f6190cb4223" - integrity sha1-CaeJk+CuTU70SH9hVakfYZDLQiM= - dependencies: - base62 "^1.1.0" - commoner "^0.10.1" - esprima-fb "^15001.1.0-dev-harmony-fb" - object-assign "^2.0.0" - source-map "^0.4.2" - -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== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== dependencies: - json-buffer "3.0.0" + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" -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= +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== dependencies: - is-buffer "^1.1.5" + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" -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= +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: - is-buffer "^1.1.5" + json-buffer "3.0.1" -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, kind-of@^6.0.3: - 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== - -klona@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -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== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - package-json "^6.3.0" - -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + language-subtag-registry "^0.3.20" -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= +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + prelude-ls "^1.2.1" + type-check "~0.4.0" lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -loader-utils@^0.2.7: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" + p-locate "^5.0.0" -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -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.mergewith@4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== -lodash@^4.0.0, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@~4.17.10: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -longest@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" - integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" -lower-case@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= - -lower-case@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.1.tgz#39eeb36e396115cc05e29422eaea9e692c9408c7" - integrity sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ== +lowlight@^1.17.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888" + integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw== dependencies: - tslib "^1.10.0" - -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== + fault "^1.0.0" + highlight.js "~10.7.0" lru-cache@^5.1.1: version "5.1.1" @@ -4127,742 +3807,487 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== +mdast-util-from-markdown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88" + integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" + integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz#daae777c72f9c4a106592e3025aa50fb26068e1b" + integrity sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^5.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== dependencies: - yallist "^4.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -make-dir@^2.0.0: +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz#1ae54d903150a10fe04d59f03b2b95fd210b2124" + integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" + integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== dependencies: - pify "^4.0.1" - semver "^5.6.0" + "@types/mdast" "^4.0.0" -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" +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -make-plural@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-4.3.0.tgz#f23de08efdb0cac2e0c9ba9f315b0dff6b4c2735" - integrity sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA== - optionalDependencies: - minimist "^1.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-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-obj@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" - integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== - -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" - -marionette.approuter@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/marionette.approuter/-/marionette.approuter-1.0.2.tgz#bd45b801762fea4ec5caa9505640413596cc432c" - integrity sha512-XjcKb1Y6KROCmdZxO/rtOdRhd3Hfrs+7zWjtfiuCFS3VZa2IQjNgKUuIGmaKDZte2AmKRRMaPvXMh22nKYFh8A== - -marionette.templatecache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/marionette.templatecache/-/marionette.templatecache-1.0.0.tgz#579b9a53b1b6428f8f0a0071cff2175a2d71e65b" - integrity sha1-V5uaU7G2Qo+PCgBxz/IXWi1x5ls= - dependencies: - backbone.marionette "^4.0.0, 4.0.0-beta.1" - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" - integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize "^1.2.0" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" - -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - -messageformat-convert@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/messageformat-convert/-/messageformat-convert-0.3.1.tgz#03b5453ee87e66da6eef1670ce0caf997ee64e51" - integrity sha512-fpNfsvFNj5VCAMN0Hpu9D4zhnGkEHL3cILJBAOydkzzdyrSoFlYIHAPRWRajOcHDgYXt+g0NIEzq0bfW3sd5Bw== - dependencies: - common-prefix "1.1.0" - make-plural "^4.3.0" - -messageformat-formatters@^2.0.1: +micromark-core-commonmark@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz#0492c1402a48775f751c9b17c0354e92be012b08" - integrity sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg== - -messageformat-loader@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/messageformat-loader/-/messageformat-loader-0.8.1.tgz#709a8f38e36257b19a9492dbfdbc743c03351fa0" - integrity sha512-hk721fJttjqoIfW6cMcLjFPsJ7C2bL9lj7Jy2btfWf7zsu6gWlLFeTKIUfi7S1v4U1gP4dkzHa1giYsAwD+aVA== + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz#9a45510557d068605c6e9a80f282b2bb8581e43d" + integrity sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" + integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== dependencies: - loader-utils "^1.2.3" - messageformat-convert "^0.3.1" - yaml "^1.6.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -messageformat-parser@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/messageformat-parser/-/messageformat-parser-4.1.3.tgz#b824787f57fcda7d50769f5b63e8d4fda68f5b9e" - integrity sha512-2fU3XDCanRqeOCkn7R5zW5VQHWf+T3hH65SzuqRvjatBK7r4uyFa5mEX+k6F9Bd04LVM5G4/BHBTUJsOdW7uyg== - -messageformat@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/messageformat/-/messageformat-2.3.0.tgz#de263c49029d5eae65d7ee25e0754f57f425ad91" - integrity sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w== - dependencies: - make-plural "^4.3.0" - messageformat-formatters "^2.0.1" - messageformat-parser "^4.1.2" - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.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" - -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromark-factory-label@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" + integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== dependencies: - braces "^3.0.1" - picomatch "^2.0.5" + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== +micromark-factory-space@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" + integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +micromark-factory-title@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" + integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +micromark-factory-whitespace@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" + integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== dependencies: - mime-db "1.44.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -mimic-fn@^2.1.0: +micromark-util-character@^2.0.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== - -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" + integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -mini-css-extract-plugin@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" - integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== +micromark-util-chunked@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" + integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== dependencies: - loader-utils "^1.1.0" - normalize-url "1.9.1" - schema-utils "^1.0.0" - webpack-sources "^1.1.0" + micromark-util-symbol "^2.0.0" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +micromark-util-classify-character@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" + integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= +micromark-util-combine-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" + integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" -"minimatch@2 || 3", minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" + integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== dependencies: - brace-expansion "^1.1.7" + micromark-util-symbol "^2.0.0" -minimist-options@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== +micromark-util-decode-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" + integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" -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== +micromark-util-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" + integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" + integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== -minipass@^3.0.0: - version "3.1.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" - integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== +micromark-util-normalize-identifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" + integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== dependencies: - yallist "^4.0.0" + micromark-util-symbol "^2.0.0" -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== +micromark-util-resolve-all@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" + integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== dependencies: - minipass "^3.0.0" - yallist "^4.0.0" + micromark-util-types "^2.0.0" -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.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== +micromark-util-sanitize-uri@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" + integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" -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== +micromark-util-subtokenize@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz#76129c49ac65da6e479c09d0ec4b5f29ec6eace5" + integrity sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q== dependencies: - minimist "^1.2.5" + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +micromark-util-symbol@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" + integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== -moment@^2.24.0: - version "2.27.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" - integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== +micromark-util-types@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" + integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= +micromark@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" + integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" + brace-expansion "^1.1.7" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -ms@^2.1.1: +moment@2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.1.2: 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== - -nan@^2.12.1, nan@^2.13.2: - 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" +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== 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= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -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== - -no-case@^2.2.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== - dependencies: - lower-case "^1.1.1" - -no-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.3.tgz#c21b434c1ffe48b39087e86cfb4d2582e9df18f8" - integrity sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw== - dependencies: - lower-case "^2.0.1" - tslib "^1.10.0" - -node-gyp@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" - integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.3" - nopt "^5.0.0" - npmlog "^4.1.2" - request "^2.88.2" - rimraf "^3.0.2" - semver "^7.3.2" - tar "^6.0.2" - which "^2.0.2" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-sass@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe" - integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ== - dependencies: - async-foreach "^0.1.3" - chalk "^1.1.1" - cross-spawn "^7.0.3" - gaze "^1.0.0" - get-stdin "^4.0.1" - glob "^7.0.3" - lodash "^4.17.15" - meow "^9.0.0" - nan "^2.13.2" - node-gyp "^7.1.0" - npmlog "^4.0.0" - request "^2.88.0" - sass-graph "2.2.5" - stdout-stream "^1.4.0" - "true-case-path" "^1.0.2" - -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@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -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-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" - integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== - dependencies: - hosted-git-info "^4.0.1" - is-core-module "^2.5.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== 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@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" - integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= - dependencies: - object-assign "^4.0.1" - prepend-http "^1.0.0" - query-string "^4.1.0" - sort-keys "^1.0.0" - -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== - -npmlog@^4.0.0, npmlog@^4.1.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" - -nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: - boolbase "~1.0.0" + path-key "^3.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= - -numeral@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" - integrity sha1-StCAk21EPCVhrtnyGX7//iX05QY= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-2.1.1.tgz#43c36e5d569ff8e4816c4efa8be02d26967c18aa" - integrity sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo= - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.1.1: 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" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -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.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -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" - -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" + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.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== +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== dependencies: - mimic-fn "^2.1.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== 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-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" -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.1, 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= - -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@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== dependencies: - p-try "^2.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +object.hasown@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== dependencies: - p-limit "^2.0.0" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.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== +object.values@^1.1.6, object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== dependencies: - p-limit "^2.2.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -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== +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + wrappy "1" -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" -param-case@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - no-case "^2.2.0" + yocto-queue "^0.1.0" -param-case@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.3.tgz#4be41f8399eff621c56eebb829a5e451d9801238" - integrity sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA== +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: - dot-case "^3.0.3" - tslib "^1.10.0" + p-limit "^3.0.2" parent-module@^1.0.0: version "1.0.1" @@ -4871,17 +4296,31 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" - integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== +parse-entities@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" + integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + +parse-entities@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + dependencies: + "@types/unist" "^2.0.0" + character-entities "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" parse-json@^5.0.0: version "5.2.0" @@ -4893,1401 +4332,724 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -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= - -pascal-case@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.1.tgz#5ac1975133ed619281e88920973d2cd1f279de5f" - integrity sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA== - dependencies: - no-case "^3.0.3" - tslib "^1.10.0" - -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-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -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, path-is-absolute@^1.0.1: +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= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -picomatch@^2.0.4, picomatch@^2.0.5, 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= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.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= - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" - integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== - dependencies: - icss-utils "^4.1.1" - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== - dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.36" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb" - integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.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@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - -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= - -pretty-error@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" - integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= - dependencies: - renderkid "^2.0.1" - utila "~0.4" - -private@^0.1.6, private@^0.1.8, private@~0.1.5: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -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.10: - 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== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -promise@^7.0.3: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -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== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -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" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - 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" - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -query-string@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" - integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= - dependencies: - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -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" - -react-dom@^0.14.0: - version "0.14.9" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.14.9.tgz#05064a3dcf0fb1880a3b2bfc9d58c55d8d9f6293" - integrity sha1-BQZKPc8PsYgKOyv8nVjFXY2fYpM= - -react@^0.14.0: - version "0.14.9" - resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" - integrity sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE= - dependencies: - envify "^3.0.0" - fbjs "^0.6.1" - -read-input@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/read-input/-/read-input-0.3.1.tgz#5b3169308013464ffda6ec92e58d2d3cea948df1" - integrity sha1-WzFpMIATRk/9puyS5Y0tPOqUjfE= - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.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" - -readable-stream@^3.1.1, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -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" - -recast@^0.11.17: - version "0.11.23" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" - integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM= - dependencies: - ast-types "0.9.6" - esprima "~3.1.0" - private "~0.1.5" - source-map "~0.5.0" - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -regenerate@^1.2.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" - integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regenerator-transform@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" - integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q== - dependencies: - babel-runtime "^6.18.0" - babel-types "^6.19.0" - private "^0.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== - -regexpu-core@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" - integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA= - dependencies: - regenerate "^1.2.1" - regjsgen "^0.2.0" - regjsparser "^0.1.4" - -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" - -regjsgen@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" - integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= - -regjsparser@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" - integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= - dependencies: - jsesc "~0.5.0" - -relateurl@0.2.x, relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -renderkid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" - integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== - dependencies: - css-select "^1.1.0" - dom-converter "^0.2" - htmlparser2 "^3.3.0" - strip-ansi "^3.0.0" - utila "^0.4.0" - -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.5.2, 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= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request@^2.88.0, request@^2.88.2: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -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-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -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@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -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.10.0, resolve@^1.3.2: - 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" - -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== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rewire@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/rewire/-/rewire-5.0.0.tgz#c4e6558206863758f6234d8f11321793ada2dbff" - integrity sha512-1zfitNyp9RH5UDyGGLe9/1N0bMlPQ0WrX0Tmg11kMHBpqwPJI4gfPpP7YngFyLbFmhXh19SToAG0sKKEFcOIJA== - dependencies: - eslint "^6.8.0" - -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - -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.5.4, rimraf@^2.6.3: - 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" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -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== - -run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -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.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - 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-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-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" +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, 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== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -sass-graph@2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" - integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== - dependencies: - glob "^7.0.0" - lodash "^4.0.0" - scss-tokenizer "^0.2.3" - yargs "^13.3.2" - -sass-loader@10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716" - integrity sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -schema-utils@^1.0.0: +possible-typed-array-names@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== -schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" -schema-utils@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -scss-tokenizer@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" - integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== dependencies: - js-base64 "^2.1.8" - source-map "^0.4.2" + fast-diff "^1.1.2" -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" +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, 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== +prismjs@^1.27.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== -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== +prismjs@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" + integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== -semver@^7.3.2, semver@^7.3.4: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== dependencies: - lru-cache "^6.0.0" - -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== - -serialize-javascript@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" - integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== - dependencies: - randombytes "^2.1.0" - -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= + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" -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== +property-information@^5.0.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" + integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + xtend "^4.0.0" -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== -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" +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== +query-string@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.0.0.tgz#1fe177cd95545600f0deab93f5fb02fd4e3e7273" + integrity sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw== dependencies: - shebang-regex "^3.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= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -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== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + decode-uri-component "^0.4.1" + filter-obj "^5.1.0" + split-on-first "^3.0.0" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -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== +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + performance-now "^2.1.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" +react-async@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/react-async/-/react-async-10.0.1.tgz#575c083f808303d2f6ca52d11ec7554dbdbd9fcd" + integrity sha512-ORUz5ca0B57QgBIzEZM5SuhJ6xFjkvEEs0gylLNlWf06vuVcLZsjIw3wx58jJkZG38p+0nUAxRgFW2b7mnVZzA== -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" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= +react-clientside-effect@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" + integrity sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg== dependencies: - is-plain-obj "^1.0.0" + "@babel/runtime" "^7.12.13" -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -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== +react-dom@18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== 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" + loose-envify "^1.1.0" + scheduler "^0.23.2" -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" +react-fast-compare@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== -source-map-support@~0.5.12: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +react-fast-compare@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" + integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== + +react-focus-lock@^2.12.1, react-focus-lock@^2.9.4: + version "2.12.1" + resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.12.1.tgz#0eaefd5fc34de8998967043d902e426352393349" + integrity sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw== + dependencies: + "@babel/runtime" "^7.0.0" + focus-lock "^1.3.5" + prop-types "^15.6.2" + react-clientside-effect "^1.2.6" + use-callback-ref "^1.3.2" + use-sidecar "^1.1.2" + +react-icons@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a" + integrity sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw== + +react-intl@^6.6.6: + version "6.6.6" + resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-6.6.6.tgz#67979f790263c5ebd95b6ea581110eea3e7b550f" + integrity sha512-dKXQNUrhZTlCp8uelYW8PHiM4saNKyLmHCfsJYWK0N/kZ/Ien35wjPHB8x9yQcTJbeN/hBOmb4x16iKUrdL9MA== + dependencies: + "@formatjs/ecma402-abstract" "1.18.2" + "@formatjs/icu-messageformat-parser" "2.7.6" + "@formatjs/intl" "2.10.2" + "@formatjs/intl-displaynames" "6.6.6" + "@formatjs/intl-listformat" "7.5.5" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/react" "16 || 17 || 18" + hoist-non-react-statics "^3.3.2" + intl-messageformat "10.5.12" + tslib "^2.4.0" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-markdown@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-9.0.1.tgz#c05ddbff67fd3b3f839f8c648e6fb35d022397d1" + integrity sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg== + dependencies: + "@types/hast" "^3.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +react-refresh@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react-remove-scroll-bar@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@^2.5.6: + version "2.5.9" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.9.tgz#6a38e7d46043abc2c6b0fb39db650b9f2e38be3e" + integrity sha512-bvHCLBrFfM2OgcrpPY2YW84sPdS2o2HKWJUf1xGyGLnSoEnOTOBpahIarjRuYtN0ryahCeP242yf+5TrBX/pZA== + dependencies: + react-remove-scroll-bar "^2.3.6" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-router-dom@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.23.0.tgz#8b80ad92ad28f4dc38972e92d84b4c208150545a" + integrity sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ== dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" + "@remix-run/router" "1.16.0" + react-router "6.23.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.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" - integrity sha1-66T12pwNyZneaAMti092FzZSA2s= +react-router@6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.23.0.tgz#2f2d7492c66a6bdf760be4c6bdf9e1d672fa154b" + integrity sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA== + dependencies: + "@remix-run/router" "1.16.0" + +react-select@5.7.7: + version "5.7.7" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.7.7.tgz#dbade9dbf711ef2a181970c10f8ab319ac37fbd0" + integrity sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" + +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + +react-syntax-highlighter@^15.5.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" + integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg== + dependencies: + "@babel/runtime" "^7.3.1" + highlight.js "^10.4.1" + lowlight "^1.17.0" + prismjs "^1.27.0" + refractor "^3.6.0" + +react-table@7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.8.0.tgz#07858c01c1718c09f7f1aed7034fcfd7bda907d2" + integrity sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA== + +react-transition-group@^4.3.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: - amdefine ">=0.0.4" + picomatch "^2.2.1" -source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +refractor@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a" + integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA== + dependencies: + hastscript "^6.0.0" + parse-entities "^2.0.0" + prismjs "~1.27.0" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.0.tgz#d5f264f42bcbd4d300f030975609d01a1697ccdc" + integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +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== -source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== +resolve@^1.19.0, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -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== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: - extend-shallow "^3.0.0" + glob "^7.1.3" -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= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -ssri@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== +rollup@^4.13.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.17.2.tgz#26d1785d0144122277fdb20ab3a24729ae68301f" + integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== dependencies: - figgy-pudding "^3.5.1" + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.17.2" + "@rollup/rollup-android-arm64" "4.17.2" + "@rollup/rollup-darwin-arm64" "4.17.2" + "@rollup/rollup-darwin-x64" "4.17.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.17.2" + "@rollup/rollup-linux-arm-musleabihf" "4.17.2" + "@rollup/rollup-linux-arm64-gnu" "4.17.2" + "@rollup/rollup-linux-arm64-musl" "4.17.2" + "@rollup/rollup-linux-powerpc64le-gnu" "4.17.2" + "@rollup/rollup-linux-riscv64-gnu" "4.17.2" + "@rollup/rollup-linux-s390x-gnu" "4.17.2" + "@rollup/rollup-linux-x64-gnu" "4.17.2" + "@rollup/rollup-linux-x64-musl" "4.17.2" + "@rollup/rollup-win32-arm64-msvc" "4.17.2" + "@rollup/rollup-win32-ia32-msvc" "4.17.2" + "@rollup/rollup-win32-x64-msvc" "4.17.2" + fsevents "~2.3.2" + +rooks@7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/rooks/-/rooks-7.14.1.tgz#f3660752c299da02eb6cc733c55f8270c3376a87" + integrity sha512-oPuLNGm3OaFm3WfZHzmDvJvRit8QrXGm9/Kn49Bz8lJUjkThSBtERWzuQ9wb5DveqrpUZvmNyBXjBE0KWVt13w== + dependencies: + fast-deep-equal "^3.1.3" + lodash.debounce "^4.0.8" + raf "^3.4.1" + use-sync-external-store "^1.2.0" -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= +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" + queue-microtask "^1.2.2" -stdout-stream@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" - integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - readable-stream "^2.0.1" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== +sass@^1.77.0: + version "1.77.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.0.tgz#e736c69aff9fae4a4e6dae60a979eee9c942f321" + integrity sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw== dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" + loose-envify "^1.1.0" -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= +semver@^7.3.4, semver@^7.5.0, semver@^7.5.4: + version "7.6.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.1.tgz#60bfe090bf907a25aa8119a72b9f90ef7ca281b2" + integrity sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA== -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= +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" -"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== +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" -string-width@^3.0.0, string-width@^3.1.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== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" + shebang-regex "^3.0.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" +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== -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" +source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -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= +space-separated-tokens@^1.0.0: + version "1.1.5" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" + integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +split-on-first@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-3.0.0.tgz#f04959c9ea8101b9b0bbf35a61b9ebea784a23e7" + integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA== + +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== dependencies: - ansi-regex "^2.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.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= +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== dependencies: - ansi-regex "^3.0.0" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" -strip-ansi@^5.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== +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== dependencies: - ansi-regex "^4.1.0" + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.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== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" -strip-indent@^3.0.0: +strip-bom@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-json-comments@^3.0.1: +strip-json-comments@^3.1.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= - -style-loader@^1.1.3: - version "1.2.1" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" - integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== +style-to-object@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.6.tgz#0c28aed8be1813d166c60d962719b2907c26547b" + integrity sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA== dependencies: - loader-utils "^2.0.0" - schema-utils "^2.6.6" + inline-style-parser "0.2.3" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.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@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - 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== + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" -syntax-error@^1.1.6: - version "1.4.0" - resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" - integrity sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w== - dependencies: - acorn-node "^1.2.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" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -"tabler-ui@git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813": - version "0.0.31" - resolved "git+https://github.com/tabler/tabler.git#00f78ad823311bc3ad974ac3e5b0126198f0a813" +synckit@^0.8.6: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== dependencies: - bootstrap "^4.0.0" + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tar@^6.0.2: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -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== - -terser-webpack-plugin@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f" - integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^3.1.0" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser@^4.1.2, terser@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== 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= - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through@^2.3.6, through@~2.3.4: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - -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" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= +tiny-invariant@^1.0.6, tiny-invariant@^1.1.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -to-fast-properties@^1.0.3: +tiny-warning@^1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tmp@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -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" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -6296,598 +5058,412 @@ to-regex-range@^5.0.1: 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" - -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" - -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== -trim-newlines@^3.0.0: +trim-lines@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -"true-case-path@^1.0.2": - version "1.0.3" - resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" - integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== - dependencies: - glob "^7.1.2" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== -tslib@^1.10.0, 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== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -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= +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== 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== + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -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== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -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== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: - is-typedarray "^1.0.0" + prelude-ls "^1.2.1" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -ua-parser-js@^0.7.9: - version "0.7.28" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" - integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -uglify-js@3.4.x: - version "3.4.10" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" - integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== dependencies: - commander "~2.19.0" - source-map "~0.6.1" + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" -uglify-js@~2.6.1: - version "2.6.4" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.6.4.tgz#65ea2fb3059c9394692f15fed87c2b36c16b9adf" - integrity sha1-ZeovswWck5RpLxX+2HwrNsFrmt8= +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== dependencies: - async "~0.2.6" - source-map "~0.5.1" - uglify-to-browserify "~1.0.0" - yargs "~3.10.0" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -uglify-to-browserify@~1.0.0: +typed-array-byte-offset@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - -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== + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== dependencies: - debug "^2.2.0" - -underscore@>=1.8.3, underscore@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" - integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" -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== +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unified@^11.0.0: + version "11.0.4" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" + integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" + "@types/unist" "^3.0.0" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== dependencies: - unique-slug "^2.0.0" + "@types/unist" "^3.0.0" -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== +unist-util-remove-position@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" + integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== dependencies: - imurmurhash "^0.1.4" + "@types/unist" "^3.0.0" + unist-util-visit "^5.0.0" -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== +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== dependencies: - crypto-random-string "^2.0.0" + "@types/unist" "^3.0.0" -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= +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -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" - -upper-case@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + "@types/unist" "^3.0.0" + unist-util-is "^6.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== +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== dependencies: - punycode "^2.1.0" + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.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= +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -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= +update-browserslist-db@^1.0.13: + version "1.0.15" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz#60ed9f8cba4a728b7ecf7356f641a31e3a691d97" + integrity sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA== dependencies: - prepend-http "^2.0.0" + escalade "^3.1.2" + picocolors "^1.0.0" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: - punycode "1.3.2" - querystring "0.2.0" + punycode "^2.1.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== +use-callback-ref@^1.3.0, use-callback-ref@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" -util-deprecate@^1.0.1, 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= +use-isomorphic-layout-effect@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== -util.promisify@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" + detect-node-es "^1.1.0" + tslib "^2.0.0" -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== dependencies: - inherits "2.0.3" + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" -utila@^0.4.0, utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= - -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== +vfile@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" + integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +vite-plugin-checker@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.6.4.tgz#aca186ab605aa15bd2c5dd9cc6d7c8fdcbe214ec" + integrity sha512-2zKHH5oxr+ye43nReRbC2fny1nyARwhxdm0uNYp/ERy4YvU9iZpNOsueoi/luXw5gnpqRSvjcEPxXbS153O2wA== + dependencies: + "@babel/code-frame" "^7.12.13" + ansi-escapes "^4.3.0" + chalk "^4.1.1" + chokidar "^3.5.1" + commander "^8.0.0" + fast-glob "^3.2.7" + fs-extra "^11.1.0" + npm-run-path "^4.0.1" + semver "^7.5.0" + strip-ansi "^6.0.0" + tiny-invariant "^1.1.0" + vscode-languageclient "^7.0.0" + vscode-languageserver "^7.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-uri "^3.0.2" + +vite@^5.2.11: + version "5.2.11" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" + integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" -v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: - 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== +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-languageserver-textdocument@^1.0.1: + version "1.0.11" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" + integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== -watchpack-chokidar2@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" - integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== - dependencies: - chokidar "^2.1.8" +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== -watchpack@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" - integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.0" + vscode-languageserver-protocol "3.16.0" -webpack-cli@^3.3.11: - version "3.3.12" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" - integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== - dependencies: - chalk "^2.4.2" - cross-spawn "^6.0.5" - enhanced-resolve "^4.1.1" - findup-sync "^3.0.0" - global-modules "^2.0.0" - import-local "^2.0.0" - interpret "^1.4.0" - loader-utils "^1.4.0" - supports-color "^6.1.0" - v8-compile-cache "^2.1.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-visualizer-plugin@^0.1.11: - version "0.1.11" - resolved "https://registry.yarnpkg.com/webpack-visualizer-plugin/-/webpack-visualizer-plugin-0.1.11.tgz#b8770ad86b4f652612c68b1b782253faf9f8a34e" - integrity sha1-uHcK2GtPZSYSxosbeCJT+vn4o04= - dependencies: - d3 "^3.5.6" - mkdirp "^0.5.1" - react "^0.14.0" - react-dom "^0.14.0" - -webpack@^4.42.1: - version "4.44.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" - integrity sha512-4UOGAohv/VGUNQJstzEywwNxqX417FnjZgZJpJQegddzPmTvph37eBIRbRTfdySXzVtJXLJfbMN3mMYhM6GdmQ== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.3.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" - -whatwg-fetch@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-0.9.0.tgz#0e3684c6cb9995b43efc9df03e4c365d95fd9cc0" - integrity sha1-DjaExsuZlbQ+/J3wPkw2XZX9nMA= - -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= +vscode-uri@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== -which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - isexe "^2.0.0" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" -which@^2.0.1, which@^2.0.2: +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 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" - -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - -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== - -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -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" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -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== - -xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: +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.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.6.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" - integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -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-parser@^20.2.3: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.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 "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^15.0.0: - 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" - -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= - dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/global/certbot-dns-plugins.js b/global/certbot-dns-plugins.js deleted file mode 100644 index 35228e6a8..000000000 --- a/global/certbot-dns-plugins.js +++ /dev/null @@ -1,515 +0,0 @@ -/** - * This file contains info about available Certbot DNS plugins. - * This only works for plugins which use the standard argument structure, so: - * --authenticator ---credentials ---propagation-seconds - * - * File Structure: - * - * { - * cloudflare: { - * display_name: "Name displayed to the user", - * package_name: "Package name in PyPi repo", - * version_requirement: "Optional package version requirements (e.g. ==1.3 or >=1.2,<2.0, see https://www.python.org/dev/peps/pep-0440/#version-specifiers)", - * dependencies: "Additional dependencies, space separated (as you would pass it to pip install)", - * credentials: `Template of the credentials file`, - * full_plugin_name: "The full plugin name as used in the commandline with certbot, e.g. 'dns-njalla'", - * }, - * ... - * } - * - */ - -module.exports = { - //####################################################// - acmedns: { - display_name: 'ACME-DNS', - package_name: 'certbot-dns-acmedns', - version_requirement: '~=0.1.0', - dependencies: '', - credentials: `dns_acmedns_api_url = http://acmedns-server/ -dns_acmedns_registration_file = /data/acme-registration.json`, - full_plugin_name: 'dns-acmedns', - }, - aliyun: { - display_name: 'Aliyun', - package_name: 'certbot-dns-aliyun', - version_requirement: '~=0.38.1', - dependencies: '', - credentials: `dns_aliyun_access_key = 12345678 -dns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef`, - full_plugin_name: 'dns-aliyun', - }, - //####################################################// - azure: { - display_name: 'Azure', - package_name: 'certbot-dns-azure', - version_requirement: '~=1.2.0', - dependencies: '', - credentials: `# This plugin supported API authentication using either Service Principals or utilizing a Managed Identity assigned to the virtual machine. -# Regardless which authentication method used, the identity will need the “DNS Zone Contributor” role assigned to it. -# As multiple Azure DNS Zones in multiple resource groups can exist, the config file needs a mapping of zone to resource group ID. Multiple zones -> ID mappings can be listed by using the key dns_azure_zoneX where X is a unique number. At least 1 zone mapping is required. - -# Using a service principal (option 1) -dns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5 -dns_azure_sp_client_secret = E-xqXU83Y-jzTI6xe9fs2YC~mck3ZzUih9 -dns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7 - -# Using used assigned MSI (option 2) -# dns_azure_msi_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5 - -# Using system assigned MSI (option 3) -# dns_azure_msi_system_assigned = true - -# Zones (at least one always required) -dns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/dns1 -dns_azure_zone2 = example.org:/subscriptions/99800903-fb14-4992-9aff-12eaf2744622/resourceGroups/dns2`, - full_plugin_name: 'dns-azure', - }, - //####################################################// - cloudflare: { - display_name: 'Cloudflare', - package_name: 'certbot-dns-cloudflare', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: 'cloudflare', - credentials: `# Cloudflare API token -dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567`, - full_plugin_name: 'dns-cloudflare', - }, - //####################################################// - cloudns: { - display_name: 'ClouDNS', - package_name: 'certbot-dns-cloudns', - version_requirement: '~=0.4.0', - dependencies: '', - credentials: `# Target user ID (see https://www.cloudns.net/api-settings/) - dns_cloudns_auth_id=1234 - # Alternatively, one of the following two options can be set: - # dns_cloudns_sub_auth_id=1234 - # dns_cloudns_sub_auth_user=foobar - - # API password - dns_cloudns_auth_password=password1`, - full_plugin_name: 'dns-cloudns', - }, - //####################################################// - cloudxns: { - display_name: 'CloudXNS', - package_name: 'certbot-dns-cloudxns', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef -dns_cloudxns_secret_key = 1122334455667788`, - full_plugin_name: 'dns-cloudxns', - }, - //####################################################// - constellix: { - display_name: 'Constellix', - package_name: 'certbot-dns-constellix', - version_requirement: '~=0.2.1', - dependencies: '', - credentials: `dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595 -dns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b -dns_constellix_endpoint = https://api.dns.constellix.com/v1`, - full_plugin_name: 'dns-constellix', - }, - //####################################################// - corenetworks: { - display_name: 'Core Networks', - package_name: 'certbot-dns-corenetworks', - version_requirement: '~=0.1.4', - dependencies: '', - credentials: `dns_corenetworks_username = asaHB12r -dns_corenetworks_password = secure_password`, - full_plugin_name: 'dns-corenetworks', - }, - //####################################################// - cpanel: { - display_name: 'cPanel', - package_name: 'certbot-dns-cpanel', - version_requirement: '~=0.2.2', - dependencies: '', - credentials: `cpanel_url = https://cpanel.example.com:2083 -cpanel_username = user -cpanel_password = hunter2`, - full_plugin_name: 'cpanel', - }, - //####################################################// - desec: { - display_name: 'deSEC', - package_name: 'certbot-dns-desec', - version_requirement: '~=0.3.0', - dependencies: '', - credentials: `dns_desec_token = YOUR_DESEC_API_TOKEN -dns_desec_endpoint = https://desec.io/api/v1/`, - full_plugin_name: 'dns-desec', - }, - //####################################################// - duckdns: { - display_name: 'DuckDNS', - package_name: 'certbot-dns-duckdns', - version_requirement: '~=0.6', - dependencies: '', - credentials: 'dns_duckdns_token=your-duckdns-token', - full_plugin_name: 'dns-duckdns', - }, - //####################################################// - digitalocean: { - display_name: 'DigitalOcean', - package_name: 'certbot-dns-digitalocean', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: 'dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff', - full_plugin_name: 'dns-digitalocean', - }, - //####################################################// - directadmin: { - display_name: 'DirectAdmin', - package_name: 'certbot-dns-directadmin', - version_requirement: '~=0.0.23', - dependencies: '', - credentials: `directadmin_url = https://my.directadminserver.com:2222 -directadmin_username = username -directadmin_password = aSuperStrongPassword`, - full_plugin_name: 'directadmin', - }, - //####################################################// - dnsimple: { - display_name: 'DNSimple', - package_name: 'certbot-dns-dnsimple', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: 'dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw', - full_plugin_name: 'dns-dnsimple', - }, - //####################################################// - dnsmadeeasy: { - display_name: 'DNS Made Easy', - package_name: 'certbot-dns-dnsmadeeasy', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a -dns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55`, - full_plugin_name: 'dns-dnsmadeeasy', - }, - //####################################################// - dnspod: { - display_name: 'DNSPod', - package_name: 'certbot-dns-dnspod', - version_requirement: '~=0.1.0', - dependencies: '', - credentials: `dns_dnspod_email = "email@example.com" -dns_dnspod_api_token = "id,key"`, - full_plugin_name: 'dns-dnspod', - }, - //####################################################// - dynu: { - display_name: 'Dynu', - package_name: 'certbot-dns-dynu', - version_requirement: '~=0.0.1', - dependencies: '', - credentials: 'dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN', - full_plugin_name: 'dns-dynu', - }, - //####################################################// - eurodns: { - display_name: 'EuroDNS', - package_name: 'certbot-dns-eurodns', - version_requirement: '~=0.0.4', - dependencies: '', - credentials: `dns_eurodns_applicationId = myuser -dns_eurodns_apiKey = mysecretpassword -dns_eurodns_endpoint = https://rest-api.eurodns.com/user-api-gateway/proxy`, - full_plugin_name: 'dns-eurodns', - }, - //####################################################// - gandi: { - display_name: 'Gandi Live DNS', - package_name: 'certbot_plugin_gandi', - version_requirement: '~=1.3.2', - dependencies: '', - credentials: `# live dns v5 api key -dns_gandi_api_key=APIKEY - -# optional organization id, remove it if not used -dns_gandi_sharing_id=SHARINGID`, - full_plugin_name: 'dns-gandi', - }, - //####################################################// - godaddy: { - display_name: 'GoDaddy', - package_name: 'certbot-dns-godaddy', - version_requirement: '~=0.2.0', - dependencies: '', - credentials: `dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567 -dns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123`, - full_plugin_name: 'dns-godaddy', - }, - //####################################################// - google: { - display_name: 'Google', - package_name: 'certbot-dns-google', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `{ -"type": "service_account", -... -}`, - full_plugin_name: 'dns-google', - }, - //####################################################// - hetzner: { - display_name: 'Hetzner', - package_name: 'certbot-dns-hetzner', - version_requirement: '~=1.0.4', - dependencies: '', - credentials: 'dns_hetzner_api_token = 0123456789abcdef0123456789abcdef', - full_plugin_name: 'dns-hetzner', - }, - //####################################################// - infomaniak: { - display_name: 'Infomaniak', - package_name: 'certbot-dns-infomaniak', - version_requirement: '~=0.1.12', - dependencies: '', - credentials: 'dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - full_plugin_name: 'dns-infomaniak', - }, - //####################################################// - inwx: { - display_name: 'INWX', - package_name: 'certbot-dns-inwx', - version_requirement: '~=2.1.2', - dependencies: '', - credentials: `dns_inwx_url = https://api.domrobot.com/xmlrpc/ -dns_inwx_username = your_username -dns_inwx_password = your_password -dns_inwx_shared_secret = your_shared_secret optional`, - full_plugin_name: 'dns-inwx', - }, - //####################################################// - ionos: { - display_name: 'IONOS', - package_name: 'certbot-dns-ionos', - version_requirement: '==2021.9.20.post1', - dependencies: '', - credentials: `dns_ionos_prefix = myapikeyprefix -dns_ionos_secret = verysecureapikeysecret -dns_ionos_endpoint = https://api.hosting.ionos.com`, - full_plugin_name: 'dns-ionos', - }, - //####################################################// - ispconfig: { - display_name: 'ISPConfig', - package_name: 'certbot-dns-ispconfig', - version_requirement: '~=0.2.0', - dependencies: '', - credentials: `dns_ispconfig_username = myremoteuser -dns_ispconfig_password = verysecureremoteuserpassword -dns_ispconfig_endpoint = https://localhost:8080`, - full_plugin_name: 'dns-ispconfig', - }, - //####################################################// - isset: { - display_name: 'Isset', - package_name: 'certbot-dns-isset', - version_requirement: '~=0.0.3', - dependencies: '', - credentials: `dns_isset_endpoint="https://customer.isset.net/api" -dns_isset_token=""`, - full_plugin_name: 'dns-isset', - }, - joker: { - display_name: 'Joker', - package_name: 'certbot-dns-joker', - version_requirement: '~=1.1.0', - dependencies: '', - credentials: `dns_joker_username = -dns_joker_password = -dns_joker_domain = `, - full_plugin_name: 'dns-joker', - }, - //####################################################// - linode: { - display_name: 'Linode', - package_name: 'certbot-dns-linode', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64 -dns_linode_version = [|3|4]`, - full_plugin_name: 'dns-linode', - }, - //####################################################// - loopia: { - display_name: 'Loopia', - package_name: 'certbot-dns-loopia', - version_requirement: '~=1.0.0', - dependencies: '', - credentials: `dns_loopia_user = user@loopiaapi -dns_loopia_password = abcdef0123456789abcdef01234567abcdef0123`, - full_plugin_name: 'dns-loopia', - }, - //####################################################// - luadns: { - display_name: 'LuaDNS', - package_name: 'certbot-dns-luadns', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `dns_luadns_email = user@example.com -dns_luadns_token = 0123456789abcdef0123456789abcdef`, - full_plugin_name: 'dns-luadns', - }, - //####################################################// - netcup: { - display_name: 'netcup', - package_name: 'certbot-dns-netcup', - version_requirement: '~=1.0.0', - dependencies: '', - credentials: `dns_netcup_customer_id = 123456 -dns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567 -dns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123`, - full_plugin_name: 'dns-netcup', - }, - //####################################################// - njalla: { - display_name: 'Njalla', - package_name: 'certbot-dns-njalla', - version_requirement: '~=1.0.0', - dependencies: '', - credentials: 'dns_njalla_token = 0123456789abcdef0123456789abcdef01234567', - full_plugin_name: 'dns-njalla', - }, - //####################################################// - nsone: { - display_name: 'NS1', - package_name: 'certbot-dns-nsone', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: 'dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw', - full_plugin_name: 'dns-nsone', - }, - //####################################################// - oci: { - display_name: 'Oracle Cloud Infrastructure DNS', - package_name: 'certbot-dns-oci', - package_version: '0.3.6', - dependencies: 'oci', - credentials: `[DEFAULT] -user = ocid1.user.oc1... -fingerprint = xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx -tenancy = ocid1.tenancy.oc1... -region = us-ashburn-1 -key_file = ~/.oci/oci_api_key.pem`, - full_plugin_name: 'dns-oci', - }, - //####################################################// - ovh: { - display_name: 'OVH', - package_name: 'certbot-dns-ovh', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `dns_ovh_endpoint = ovh-eu -dns_ovh_application_key = MDAwMDAwMDAwMDAw -dns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw -dns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw`, - full_plugin_name: 'dns-ovh', - }, - //####################################################// - porkbun: { - display_name: 'Porkbun', - package_name: 'certbot-dns-porkbun', - version_requirement: '~=0.2', - dependencies: '', - credentials: `dns_porkbun_key=your-porkbun-api-key -dns_porkbun_secret=your-porkbun-api-secret`, - full_plugin_name: 'dns-porkbun', - }, - //####################################################// - powerdns: { - display_name: 'PowerDNS', - package_name: 'certbot-dns-powerdns', - version_requirement: '~=0.2.0', - dependencies: '', - credentials: `dns_powerdns_api_url = https://api.mypowerdns.example.org -dns_powerdns_api_key = AbCbASsd!@34`, - full_plugin_name: 'dns-powerdns', - }, - //####################################################// - regru: { - display_name: 'reg.ru', - package_name: 'certbot-regru', - version_requirement: '~=1.0.2', - dependencies: '', - credentials: `certbot_regru:dns_username=username -certbot_regru:dns_password=password`, - full_plugin_name: 'certbot-regru:dns', - }, - //####################################################// - rfc2136: { - display_name: 'RFC 2136', - package_name: 'certbot-dns-rfc2136', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `# Target DNS server -dns_rfc2136_server = 192.0.2.1 -# Target DNS port -dns_rfc2136_port = 53 -# TSIG key name -dns_rfc2136_name = keyname. -# TSIG key secret -dns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg== -# TSIG key algorithm -dns_rfc2136_algorithm = HMAC-SHA512`, - full_plugin_name: 'dns-rfc2136', - }, - //####################################################// - route53: { - display_name: 'Route 53 (Amazon)', - package_name: 'certbot-dns-route53', - version_requirement: '==$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')', // official plugin, use certbot version - dependencies: '', - credentials: `[default] -aws_access_key_id=AKIAIOSFODNN7EXAMPLE -aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`, - full_plugin_name: 'dns-route53', - }, - //####################################################// - transip: { - display_name: 'TransIP', - package_name: 'certbot-dns-transip', - version_requirement: '~=0.4.3', - dependencies: '', - credentials: `dns_transip_username = my_username -dns_transip_key_file = /etc/letsencrypt/transip-rsa.key`, - full_plugin_name: 'dns-transip', - }, - //####################################################// - tencentcloud: { - display_name: 'Tencent Cloud', - package_name: 'certbot-dns-tencentcloud', - version_requirement: '~=2.0.0', - dependencies: '', - credentials: `dns_tencentcloud_secret_id = TENCENT_CLOUD_SECRET_ID -dns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY`, - full_plugin_name: 'dns-tencentcloud', - }, - //####################################################// - vultr: { - display_name: 'Vultr', - package_name: 'certbot-dns-vultr', - version_requirement: '~=1.0.3', - dependencies: '', - credentials: 'dns_vultr_key = YOUR_VULTR_API_KEY', - full_plugin_name: 'dns-vultr', - }, - //####################################################// - websupportsk: { - display_name: 'Websupport.sk', - package_name: 'certbot-dns-websupportsk', - version_requirement: '~=0.1.6', - dependencies: '', - credentials: `dns_websupportsk_api_key = -dns_websupportsk_secret = -dns_websupportsk_domain = example.com`, - full_plugin_name: 'dns-websupportsk', - }, -}; diff --git a/scripts/.common.sh b/scripts/.common.sh index 3cea09167..7aabb6059 100644 --- a/scripts/.common.sh +++ b/scripts/.common.sh @@ -1,12 +1,12 @@ #!/bin/bash # Colors -BLUE='\E[1;34m' -CYAN='\E[1;36m' -GREEN='\E[1;32m' -RED='\E[1;31m' -RESET='\E[0m' -YELLOW='\E[1;33m' +BLUE='\033[1;34m' +CYAN='\033[1;36m' +GREEN='\033[1;32m' +RED='\033[1;31m' +RESET='\033[0m' +YELLOW='\033[1;33m' export BLUE CYAN GREEN RED RESET YELLOW diff --git a/scripts/buildx b/scripts/buildx index 4da6c1674..d687c20be 100755 --- a/scripts/buildx +++ b/scripts/buildx @@ -5,7 +5,6 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo -e "${BLUE}❯ ${CYAN}Building docker multiarch: ${YELLOW}${*}${RESET}" -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "${DIR}/.." || exit 1 # determine commit if not already set @@ -18,12 +17,14 @@ docker buildx create --name "${BUILDX_NAME:-npm}" || echo docker buildx use "${BUILDX_NAME:-npm}" docker buildx build \ - --build-arg BUILD_VERSION="${BUILD_VERSION:-dev}" \ --build-arg BUILD_COMMIT="${BUILD_COMMIT:-notset}" \ --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \ - --build-arg GOPROXY="${GOPROXY:-}" \ + --build-arg BUILD_VERSION="${BUILD_VERSION:-dev}" \ + --build-arg NOW="$(date --rfc-3339=s)" \ + --build-arg SKIP_TESTS=1 \ --build-arg GOPRIVATE="${GOPRIVATE:-}" \ - --platform linux/amd64,linux/arm64,linux/arm/7 \ + --build-arg GOPROXY="${GOPROXY:-}" \ + --platform linux/amd64,linux/arm64 \ --progress plain \ --pull \ -f docker/Dockerfile \ diff --git a/scripts/ci/build-backend b/scripts/ci/build-backend new file mode 100755 index 000000000..f266424bf --- /dev/null +++ b/scripts/ci/build-backend @@ -0,0 +1,81 @@ +#!/bin/bash +set -e +set +x + +IMAGE=jc21/gotools:latest + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +BUILD_DATE=$(date '+%Y-%m-%d %T %Z') +NOW=$(date --rfc-3339=s) + +cd $DIR/../.. + +if [ "$BUILD_COMMIT" = "" ]; then + BUILD_COMMIT=$(git log -n 1 --format=%h) +fi + +if [ "$BUILD_VERSION" = "" ]; then + BUILD_VERSION=$(cat .version) +fi + +export CGO_ENABLED=0 +export GO111MODULE=on + +echo -e "${BLUE}❯ ${GREEN}build-backend:${RESET}" +echo " BUILD_COMMIT: $BUILD_COMMIT" +echo " BUILD_DATE: $BUILD_DATE" +echo " BUILD_VERSION: $BUILD_VERSION" +echo " CGO_ENABLED: ${CGO_ENABLED:-}" +echo " GO111MODULE: ${GO111MODULE:-}" +echo " GOPRIVATE: ${GOPRIVATE:-}" +echo " GOPROXY: ${GOPROXY:-}" +echo " NOW: $NOW" + +cleanup() { + docker run --rm -v "$(pwd):/app" "${IMAGE}" chown -R "$(id -u):$(id -g)" /app/bin +} + +build_backend() { + echo -e "${BLUE}❯ ${CYAN}Building backend for ${YELLOW}${1}-${2} ...${RESET}" + + FILENAME="nginxproxymanager-${1}_${2}" + if [ "$1" = "windows" ]; then + FILENAME="${FILENAME}.exe" + fi + + docker run --rm \ + -e BUILD_COMMIT="${BUILD_COMMIT:-notset}" \ + -e BUILD_DATE="$BUILD_DATE" \ + -e GOARCH="${2}" \ + -e GOOS="${1}" \ + -e GOPRIVATE="${GOPRIVATE:-}" \ + -e GOPROXY="${GOPROXY:-}" \ + -e NOW="$NOW" \ + -e TZ="${TZ:-Australia/Brisbane}" \ + -v "$(pwd):/app" \ + -w '/app/backend' \ + "${IMAGE}" \ + go build \ + -tags 'json1' \ + -buildvcs=false \ + -ldflags "-w -s -X main.commit=${BUILD_COMMIT:-notset} -X main.version=${BUILD_VERSION}" \ + -o "/app/bin/$FILENAME" \ + ./cmd/server +} + +docker pull "${IMAGE}" + +#build_backend "darwin" "amd64" +#build_backend "darwin" "arm64" +build_backend "linux" "amd64" +build_backend "linux" "arm64" +build_backend "linux" "arm" +#build_backend "openbsd" "amd64" +#build_backend "windows" "amd64" + +cleanup + +echo -e "${BLUE}❯ ${GREEN}build-backend completed${RESET}" +exit 0 diff --git a/scripts/ci/build-cleanup b/scripts/ci/build-cleanup new file mode 100755 index 000000000..2c0c75df4 --- /dev/null +++ b/scripts/ci/build-cleanup @@ -0,0 +1,19 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +# Ensure docker exists +if hash docker 2>/dev/null; then + cd "${DIR}/../.." + echo -e "${BLUE}❯ ${CYAN}Build Cleanup ...${RESET}" + + docker run --rm -e CI=true -v "$(pwd):/app" -w /app jc21/gotools:latest rm -rf \ + /app/frontend/node_modules \ + /app/docs/node_modules \ + /app/docs/dist + + echo -e "${BLUE}❯ ${GREEN}Build Cleanup Complete${RESET}" +else + echo -e "${RED}❯ docker command is not available${RESET}" +fi diff --git a/scripts/ci/build-frontend b/scripts/ci/build-frontend new file mode 100755 index 000000000..73e89b6a6 --- /dev/null +++ b/scripts/ci/build-frontend @@ -0,0 +1,49 @@ +#!/bin/bash +set -e + +BACKEND_ASSETS=backend/embed/assets + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +docker_cmd() { + docker run --rm \ + -e CI=true \ + -e "VITE_APP_VERSION=${BUILD_VERSION:-0.0.0}" \ + -e "VITE_APP_COMMIT=${BUILD_COMMIT:-0000000}" \ + -v "$(pwd):/app" \ + -w "/app/frontend" \ + node:20 \ + ${*} +} + +cd "${DIR}/../.." || exit 1 +echo -e "${BLUE}❯ ${CYAN}Installing Frontend deps ...${RESET}" +rm -rf frontend/node_modules +docker_cmd yarn install + +echo -e "${BLUE}❯ ${CYAN}Checking locales ...${RESET}" +docker_cmd node check-locales.js + +echo -e "${BLUE}❯ ${CYAN}Compiling locales ...${RESET}" +docker_cmd yarn locale-compile +docker_cmd chown -R "$(id -u):$(id -g)" /app/frontend/src/locale/lang + +echo -e "${BLUE}❯ ${CYAN}Running eslint ...${RESET}" +docker_cmd yarn eslint src +docker_cmd yarn eslint -f junit src -o eslint.xml +#echo -e "${BLUE}❯ ${CYAN}Running tests ...${RESET}" +#docker_cmd yarn test --coverage --watchAll=false --testResultsProcessor ./node_modules/jest-junit +echo -e "${BLUE}❯ ${CYAN}Building Frontend ...${RESET}" +docker_cmd yarn build +docker_cmd chown -R "$(id -u):$(id -g)" /app/frontend +echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}" + +# to avoid CRA ejection, just copy these build files over to embed in the backend +rm -rf ${BACKEND_ASSETS} +cp -pr frontend/dist "${BACKEND_ASSETS}" +echo -e "${BLUE}❯ ${GREEN}Copied build to ${BACKEND_ASSETS}${RESET}" + +rm -rf frontend/node_modules + +trap "docker_cmd chown -R $(id -u):$(id -g) /app/frontend" EXIT diff --git a/scripts/ci/fulltest-cypress b/scripts/ci/fulltest-cypress new file mode 100755 index 000000000..c4a0844f9 --- /dev/null +++ b/scripts/ci/fulltest-cypress @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +STACK="${1:-}" + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# remember this is running in "ci" folder.. + +# Some defaults for running this script outside of CI +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-npm_local_fulltest}" +export IMAGE="${IMAGE:-nginx-proxy-manager}" +export BRANCH_LOWER="${BRANCH_LOWER:-unknown}" +export BUILD_NUMBER="${BUILD_NUMBER:-0000}" + +if [ "${COMPOSE_FILE:-}" = "" ]; then + export COMPOSE_FILE="docker/docker-compose.ci.yml" + if [ "$STACK" != "" ]; then + export COMPOSE_FILE="${COMPOSE_FILE}:docker/docker-compose.ci.${STACK}.yml" + fi +fi + +# Colors +BLUE='\E[1;34m' +RED='\E[1;31m' +CYAN='\E[1;36m' +GREEN='\E[1;32m' +RESET='\E[0m' +YELLOW='\E[1;33m' + +export BLUE CYAN GREEN RESET YELLOW + +echo -e "${BLUE}❯ ${CYAN}Starting fullstack cypress testing ...${RESET}" +echo -e "${BLUE}❯ $(docker-compose config)${RESET}" + +# $1: container_name +get_container_ip () { + local container_name=$1 + local container + local ip + container=$(docker-compose ps --all -q "${container_name}" | tail -n1) + ip=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$container") + echo "$ip" +} + +# Bring up a stack, in steps so we can inject IPs everywhere +docker-compose up -d pdns pdns-db +PDNS_IP=$(get_container_ip "pdns") +echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}" + +# adjust the dnsrouter config +LOCAL_DNSROUTER_CONFIG="$DIR/../../docker/dev/dnsrouter-config.json" +rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp" +# IMPORTANT: changes to dnsrouter-config.json will affect this line: +jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp" + +docker-compose up -d dnsrouter +DNSROUTER_IP=$(get_container_ip "dnsrouter") +echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}" + +if [ "${DNSROUTER_IP:-}" = "" ]; then + echo -e "${RED}❯ ERROR: DNS Router IP is not set${RESET}" + exit 1 +fi + +# mount the resolver +LOCAL_RESOLVE="$DIR/../../docker/dev/resolv.conf" +rm -rf "${LOCAL_RESOLVE}" +printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" + +# bring up all remaining containers, except cypress! +docker-compose up -d --remove-orphans stepca +docker-compose pull db-mysql || true # ok to fail +docker-compose pull db-postgres || true # ok to fail +docker-compose pull authentik authentik-redis authentik-ldap || true # ok to fail +docker-compose up -d --remove-orphans --pull=never fullstack + +# wait for main container to be healthy +bash "$DIR/../wait-healthy" "$(docker-compose ps --all -q fullstack)" 120 + +# Wait for authentik to be healthy, if it exists as a compose service +if [ "$(docker-compose ps --all -q authentik)" != "" ]; then + bash "$DIR/../wait-healthy" "$(docker-compose ps --all -q authentik)" 90 'true' +fi + +# Run tests +rm -rf "$DIR/../../test/results" +docker-compose up --build cypress + +# Get results +docker cp -L "$(docker-compose ps --all -q cypress):/test/results" "$DIR/../../test/" +docker cp -L "$(docker-compose ps --all -q fullstack):/data/logs" "$DIR/../../test/results/" + +if [ "$2" = "cleanup" ]; then + echo -e "${BLUE}❯ ${CYAN}Cleaning up containers ...${RESET}" + docker-compose down --remove-orphans --volumes -t 30 +fi + +echo -e "${BLUE}❯ ${GREEN}Fullstack cypress testing complete${RESET}" diff --git a/scripts/ci/test-backend b/scripts/ci/test-backend new file mode 100755 index 000000000..23a5c35c6 --- /dev/null +++ b/scripts/ci/test-backend @@ -0,0 +1,78 @@ +#!/bin/bash +set -e + +IMAGE=jc21/gotools:latest + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/../.common.sh" + +BUILD_DATE=$(date '+%Y-%m-%d %T %Z') +NOW=$(date --rfc-3339=s) +cd $DIR/../.. + +if [ "$BUILD_COMMIT" = "" ]; then + BUILD_COMMIT=$(git log -n 1 --format=%h) +fi + +if [ "$BUILD_VERSION" = "" ]; then + BUILD_VERSION=$(cat .version) +fi + +echo -e "${BLUE}❯ ${GREEN}test-backend: ${YELLOW}${1:-}${RESET}" +echo " BUILD_COMMIT: ${BUILD_COMMIT:-notset}" +echo " BUILD_DATE: $BUILD_DATE" +echo " BUILD_VERSION: $BUILD_VERSION" +echo " CGO_ENABLED: ${CGO_ENABLED:-not set}" +echo " GO111MODULE: ${GO111MODULE:-}" +echo " GOPRIVATE: ${GOPRIVATE:-}" +echo " GOPROXY: ${GOPROXY:-}" +echo " NOW: $NOW" + +if [ "${1:-}" = "--inside-docker" ]; then + trap cleanup EXIT + cleanup() { + rm -f "/tmp/coverage.out" + chown -R 1000:1000 "$DIR/../../test/results" + } + + mkdir -p /workspace + echo -e "${BLUE}❯ ${CYAN}govulncheck setup${RESET}" + cd /workspace + cp /app/backend/go.mod /app/backend/go.sum . + go mod download + + echo -e "${BLUE}❯ ${CYAN}govulncheck testing${RESET}" + govulncheck ./... + rm -rf /workspace + + echo -e "${BLUE}❯ ${CYAN}Testing backend code${RESET}" + cd /app/backend + [ -z "$(go tool fix -diff ./internal)" ] + go test -json -cover -coverprofile="/tmp/coverage.out" ./... | tparse + mkdir -p "$DIR/../../test/results/html-reports" "$DIR/../../test/results/junit" + go tool cover -html="/tmp/coverage.out" -o "$DIR/../../test/results/html-reports/backend-coverage.html" + go test -v -tags="unit integration" -covermode=atomic ./internal/... 2>&1 | go-junit-report -set-exit-code > "$DIR/../../test/results/junit/backend.xml" + go-test-coverage -c .testcoverage.yml --p "/tmp/coverage.out" + # disabled as it can't handle -buildvcs=false flag and complains about it: + # golangci-lint -v run ./... +else + # run this script from within docker + docker pull "${IMAGE}" + docker run --rm \ + -e BUILD_COMMIT="${BUILD_COMMIT:-notset}" \ + -e BUILD_DATE="$BUILD_DATE" \ + -e BUILD_VERSION="$BUILD_VERSION" \ + -e GOARCH="${2}" \ + -e GOOS="${1}" \ + -e GOPRIVATE="${GOPRIVATE:-}" \ + -e GOPROXY="${GOPROXY:-}" \ + -e NOW="$NOW" \ + -e TZ="${TZ:-Australia/Brisbane}" \ + -v "$(pwd):/app" \ + -w '/app/backend' \ + "${IMAGE}" \ + /app/scripts/ci/test-backend --inside-docker +fi + +echo -e "${BLUE}❯ ${GREEN}test-backend ${YELLOW}${1:-} ${GREEN}completed${RESET}" +exit 0 diff --git a/scripts/docs-build b/scripts/docs-build index 990313912..13d061db7 100755 --- a/scripts/docs-build +++ b/scripts/docs-build @@ -7,7 +7,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if hash docker 2>/dev/null; then cd "${DIR}/.." echo -e "${BLUE}❯ ${CYAN}Building Docs ...${RESET}" - docker run --rm -e CI=true -v "$(pwd)/docs:/app/docs" -w /app/docs node:alpine sh -c "yarn install && yarn build && chown -R $(id -u):$(id -g) /app/docs" + docker run --rm -e CI=true -v "$(pwd)/docs:/app/docs" -w /app/docs node:latest sh -c "yarn install && yarn build && chown -R $(id -u):$(id -g) /app/docs" echo -e "${BLUE}❯ ${GREEN}Building Docs Complete${RESET}" else echo -e "${RED}❯ docker command is not available${RESET}" diff --git a/scripts/frontend-build b/scripts/frontend-build deleted file mode 100755 index 0e12cf068..000000000 --- a/scripts/frontend-build +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -e - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -. "$DIR/.common.sh" - -DOCKER_IMAGE=nginxproxymanager/nginx-full:certbot-node - -# Ensure docker exists -if hash docker 2>/dev/null; then - docker pull "${DOCKER_IMAGE}" - cd "${DIR}/.." - echo -e "${BLUE}❯ ${CYAN}Building Frontend ...${RESET}" - docker run --rm -e CI=true -v "$(pwd)/frontend:/app/frontend" -v "$(pwd)/global:/app/global" -w /app/frontend "$DOCKER_IMAGE" sh -c "yarn install && yarn build && yarn build && chown -R $(id -u):$(id -g) /app/frontend" - echo -e "${BLUE}❯ ${GREEN}Building Frontend Complete${RESET}" -else - echo -e "${RED}❯ docker command is not available${RESET}" -fi diff --git a/scripts/frontend-lint b/scripts/frontend-lint new file mode 100755 index 000000000..abc64314e --- /dev/null +++ b/scripts/frontend-lint @@ -0,0 +1,17 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Ensure docker-compose exists +if hash docker 2>/dev/null; then + cd "${DIR}/.." + echo -e "${BLUE}❯ ${CYAN}Linting Frontend ...${RESET}" + docker run --rm -e CI=true -v "$(pwd)/frontend:/app/frontend" -w /app/frontend node:20 sh -c "yarn install && yarn eslint src;chown -R $(id -u):$(id -g) /app/frontend" + RES=$? + echo -e "${BLUE}❯ ${GREEN}Linting Frontend Complete${RESET}" + exit $RES +else + echo -e "${RED}❯ docker command is not available${RESET}" + exit 1 +fi diff --git a/scripts/go-multiarch-wrapper b/scripts/go-multiarch-wrapper new file mode 100755 index 000000000..ef0c9cb28 --- /dev/null +++ b/scripts/go-multiarch-wrapper @@ -0,0 +1,44 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +export GOOS=linux + +# Determine the correct binary file for the architecture given +case ${TARGETPLATFORM:-} in + linux/arm64) + export GOARCH=arm64 + ;; + + linux/arm/v7) + export GOARCH=arm + ;; + + linux/amd64) + export GOARCH=amd64 + ;; +esac + +echo -e "${BLUE}❯ ${CYAN}Building binaries for ${YELLOW}${GOARCH} (${TARGETPLATFORM:-})${RESET}" + +# server +go build \ + -tags 'json1' \ + -buildvcs=false \ + -ldflags "-w -s -X main.commit=${BUILD_COMMIT:-notset} -X main.version=${BUILD_VERSION}" \ + -o "${1:-/dist/server}" \ + ./cmd/server + +# ipranges +go build \ + -buildvcs=false \ + -ldflags "-w -s -X main.commit=${BUILD_COMMIT:-notset} -X main.version=${BUILD_VERSION}" \ + -o "${2:-/dist/ipranges}" \ + ./cmd/ipranges + +# test binaries +/dist/server --version +/dist/ipranges --version + +echo -e "${BLUE}❯ ${CYAN}Build binaries complete${RESET}" diff --git a/scripts/install-s6 b/scripts/install-s6 index 8bb85e421..22e792f8e 100755 --- a/scripts/install-s6 +++ b/scripts/install-s6 @@ -8,8 +8,8 @@ BLUE='\E[1;34m' GREEN='\E[1;32m' RESET='\E[0m' -S6_OVERLAY_VERSION=1.22.1.0 -TARGETPLATFORM=$1 +S6_OVERLAY_VERSION=3.2.0.0 +TARGETPLATFORM=${1:-linux/amd64} # Determine the correct binary file for the architecture given case $TARGETPLATFORM in @@ -22,13 +22,17 @@ case $TARGETPLATFORM in ;; *) - S6_ARCH=amd64 + S6_ARCH=x86_64 ;; esac echo -e "${BLUE}❯ ${CYAN}Installing S6-overlay v${S6_OVERLAY_VERSION} for ${YELLOW}${TARGETPLATFORM} (${S6_ARCH})${RESET}" -curl -L -o "/tmp/s6-overlay-${S6_ARCH}.tar.gz" "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.gz" \ - && tar -xzf "/tmp/s6-overlay-${S6_ARCH}.tar.gz" -C / +curl -L -o '/tmp/s6-overlay-noarch.tar.xz' "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" +curl -L -o "/tmp/s6-overlay-${S6_ARCH}.tar.xz" "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" +tar -C / -Jxpf '/tmp/s6-overlay-noarch.tar.xz' +tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" + +rm -rf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" echo -e "${BLUE}❯ ${GREEN}S6-overlay install Complete${RESET}" diff --git a/scripts/sqlite b/scripts/sqlite new file mode 100755 index 000000000..69a54055b --- /dev/null +++ b/scripts/sqlite @@ -0,0 +1,17 @@ +#!/bin/bash -e + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$DIR/.common.sh" + +# Ensure docker-compose exists +if hash docker-compose 2>/dev/null; then + cd "${DIR}/.." + + echo "" + inform "Tip: List tables with: \\dt" + echo "" + + docker-compose exec npm usql /data/nginxproxymanager.db +else + echo -e "${RED}❯ docker-compose command is not available${RESET}" +fi diff --git a/scripts/start-dev b/scripts/start-dev index f064a4bdf..0411c61a3 100755 --- a/scripts/start-dev +++ b/scripts/start-dev @@ -1,27 +1,55 @@ -#!/bin/bash -e +#!/usr/bin/env bash +set -e DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" . "$DIR/.common.sh" +get_container_ip () { + local container_name=$1 + local container + local ip + container=$(docker-compose ps --all -q "${container_name}" | tail -n1) + ip=$(docker inspect -f "{{.NetworkSettings.Networks.npmdev_default.IPAddress}}" "$container") + echo "$ip" +} + # Ensure docker-compose exists if hash docker-compose 2>/dev/null; then cd "${DIR}/.." echo -e "${BLUE}❯ ${CYAN}Starting Dev Stack ...${RESET}" - docker-compose up -d --remove-orphans --force-recreate --build + # Bring up a stack, in steps so we can inject IPs everywhere + docker-compose up -d npm-pdns npm-pdns-db + PDNS_IP=$(get_container_ip 'npm-pdns') + echo -e "${BLUE}❯ ${YELLOW}PDNS IP is ${PDNS_IP}${RESET}" + + # adjust the dnsrouter config + LOCAL_DNSROUTER_CONFIG="$DIR/../docker/dev/dnsrouter-config.json" + rm -rf "$LOCAL_DNSROUTER_CONFIG.tmp" + # IMPORTANT: changes to dnsrouter-config.json will affect this line: + jq --arg a "$PDNS_IP" '.servers[0].upstreams[1].upstream = $a' "$LOCAL_DNSROUTER_CONFIG" > "$LOCAL_DNSROUTER_CONFIG.tmp" + + # dnsrouter + docker-compose up -d npm-dnsrouter + DNSROUTER_IP=$(get_container_ip 'npm-dnsrouter') + echo -e "${BLUE}❯ ${YELLOW}DNS Router IP is ${DNSROUTER_IP}${RESET}" + + # mount the resolver + LOCAL_RESOLVE="$DIR/../docker/dev/resolv.conf" + rm -rf "${LOCAL_RESOLVE}" + printf "nameserver %s\noptions ndots:0" "${DNSROUTER_IP}" > "${LOCAL_RESOLVE}" - echo "" - echo -e "${CYAN}Admin UI: http://127.0.0.1:3081${RESET}" - echo -e "${CYAN}Nginx: http://127.0.0.1:3080${RESET}" - echo -e "${CYAN}Swagger Doc: http://127.0.0.1:3001${RESET}" - echo "" + # bring things up, but only what we haven't already created + docker-compose up -d --remove-orphans --force-recreate --build npm npm-pebble npm-stepca npm-swagger if [ "$1" == "-f" ]; then echo -e "${BLUE}❯ ${YELLOW}Following Backend Container:${RESET}" - docker logs -f npm_core + docker logs -f npm.dev else - echo -e "${YELLOW}Hint:${RESET} You can follow the output of some of the containers with:" - echo " docker logs -f npm_core" + echo -e "${YELLOW}Tip:${RESET} You can follow the output of some of the containers with:" + echo " docker logs -f npm.dev" + echo -e "${YELLOW}Tip:${RESET} Open a database terminal with:" + echo " ./scripts/sqlite" fi else echo -e "${RED}❯ docker-compose command is not available${RESET}" diff --git a/scripts/test-dev b/scripts/test-dev index f75527b7e..eb5c5bd36 100755 --- a/scripts/test-dev +++ b/scripts/test-dev @@ -7,7 +7,7 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if hash docker-compose 2>/dev/null; then cd "${DIR}/.." echo -e "${BLUE}❯ ${CYAN}Testing Dev Stack ...${RESET}" - docker-compose exec -T npm bash -c "cd /app && task test" + docker-compose exec -T npm bash -c "cd /app/backend && task test" else echo -e "${RED}❯ docker-compose command is not available${RESET}" fi diff --git a/scripts/wait-healthy b/scripts/wait-healthy index b8da5d69b..8fc3d9b02 100755 --- a/scripts/wait-healthy +++ b/scripts/wait-healthy @@ -5,29 +5,35 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ "$1" == "" ]; then echo "Waits for a docker container to be healthy." - echo "Usage: $0 docker-container" + echo " Usage: $0 docker-container 30" + echo "or use the third parameter to use the docker healthcheck instead of the internal one." + echo " Usage: $0 docker-container 30 true" exit 1 fi SERVICE=$1 -LOOPCOUNT=0 -HEALTHY= LIMIT=${2:-90} +USE_DOCKER_HEALTHCHECK=${3:-false} echo -e "${BLUE}❯ ${CYAN}Waiting for healthy: ${YELLOW}${SERVICE}${RESET}" -until [ "${HEALTHY}" = "healthy" ]; do - echo -n "." - sleep 1 - HEALTHY="$(docker inspect -f '{{.State.Health.Status}}' $SERVICE)" - ((LOOPCOUNT++)) +is_up() { + if [ "$USE_DOCKER_HEALTHCHECK" == "true" ]; then + docker inspect --format='{{.State.Health.Status}}' "$SERVICE" | grep -qi "healthy" + else + docker exec "$SERVICE" /bin/healthcheck.sh + fi +} - if [ "$LOOPCOUNT" == "$LIMIT" ]; then - echo "" - echo "" +i=0 +while ! is_up; do + i=$((i + 1)) + if [ "$i" == "$LIMIT" ]; then echo -e "${BLUE}❯ ${RED}Timed out waiting for healthy${RESET}" + docker logs --tail 50 "$SERVICE" exit 1 fi + sleep 1 done echo "" diff --git a/test/.dockerignore b/test/.dockerignore deleted file mode 100644 index b512c09d4..000000000 --- a/test/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/test/.gitignore b/test/.gitignore index fccdd3810..bd6b6b9c1 100644 --- a/test/.gitignore +++ b/test/.gitignore @@ -1,4 +1,2 @@ -.vscode -node_modules -results -cypress/videos +results/* +cypress/results/* diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile index be04748a2..b61784f1d 100644 --- a/test/cypress/Dockerfile +++ b/test/cypress/Dockerfile @@ -1,13 +1,14 @@ -FROM cypress/included:5.6.0 +FROM cypress/included:13.15.1 -COPY --chown=1000 ./ /test +COPY --chown=1000 ./test /test -# mkcert -ENV MKCERT=1.4.2 -RUN wget -O /usr/bin/mkcert "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT}/mkcert-v${MKCERT}-linux-amd64" \ - && chmod +x /usr/bin/mkcert +# Disable Cypress CLI colors by default +ARG FORCE_COLOR=0 +ARG NO_COLOR=1 +ENV FORCE_COLOR=$FORCE_COLOR \ + NO_COLOR=$NO_COLOR WORKDIR /test -RUN yarn install +RUN yarn install && yarn cache clean ENTRYPOINT [] CMD ["cypress", "run"] diff --git a/test/cypress/config/ci.js b/test/cypress/config/ci.js new file mode 100644 index 000000000..5f413cf36 --- /dev/null +++ b/test/cypress/config/ci.js @@ -0,0 +1,22 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + requestTimeout: 30000, + defaultCommandTimeout: 20000, + reporter: 'cypress-multi-reporters', + reporterOptions: { + configFile: 'multi-reporter.json' + }, + video: true, + videosFolder: 'results/videos', + screenshotsFolder: 'results/screenshots', + e2e: { + setupNodeEvents(on, config) { + return require('../plugins/index.js')(on, config); + }, + env: { + swaggerBase: '{{baseUrl}}/api/schema', + }, + baseUrl: 'http://localhost:1234', + }, +}); diff --git a/test/cypress/config/ci.json b/test/cypress/config/ci.json deleted file mode 100644 index e11df476b..000000000 --- a/test/cypress/config/ci.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "requestTimeout": 30000, - "defaultCommandTimeout": 20000, - "reporter": "cypress-multi-reporters", - "reporterOptions": { - "configFile": "multi-reporter.json" - }, - "videosFolder": "results/videos", - "screenshotsFolder": "results/screenshots", - "env": { - "swaggerBase": "{{baseUrl}}/api/schema", - "RETRIES": 4 - } -} diff --git a/test/cypress/config/dev.js b/test/cypress/config/dev.js new file mode 100644 index 000000000..93ff18d63 --- /dev/null +++ b/test/cypress/config/dev.js @@ -0,0 +1,22 @@ +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + requestTimeout: 30000, + defaultCommandTimeout: 20000, + reporter: 'cypress-multi-reporters', + reporterOptions: { + configFile: 'multi-reporter.json' + }, + video: true, + videosFolder: 'results/videos', + screenshotsFolder: 'results/screenshots', + e2e: { + setupNodeEvents(on, config) { + return require('../plugins/index.js')(on, config); + }, + env: { + swaggerBase: '{{baseUrl}}/api/schema', + skipStackCheck: 'true', + }, + } +}); diff --git a/test/cypress/config/dev.json b/test/cypress/config/dev.json deleted file mode 100644 index 19492c345..000000000 --- a/test/cypress/config/dev.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "requestTimeout": 30000, - "defaultCommandTimeout": 20000, - "reporter": "cypress-multi-reporters", - "reporterOptions": { - "configFile": "multi-reporter.json" - }, - "videos": false, - "screenshotsFolder": "results/screenshots", - "env": { - "swaggerBase": "{{baseUrl}}/api/schema", - "RETRIES": 0 - } -} diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js new file mode 100644 index 000000000..e79b5ec74 --- /dev/null +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -0,0 +1,126 @@ +/// + +describe('Certificates endpoints', () => { + let token; + let certID; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create new certificate', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/certificates', + data: { + type: 'http', + certificate_authority_id: 1, + name: 'My First Cert', + domain_names: [ + 'jc21.com', + '*.google.com' + ] + } + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('post', 201, '/certificates', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + expect(data.result.user_id).to.be.greaterThan(0); + certID = data.result.id; + }); + }); + + it('Should be able to get a certificate', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/certificates/' + certID + '?expand=user' + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/certificates/{certificateID}', data); + expect(data.result).to.have.property('id', certID); + expect(data.result).to.have.property('user'); + expect(data.result.user).to.have.property('gravatar_url'); + expect(data.result.user.gravatar_url).to.include('gravatar.com'); + }); + }); + + it('Should be able to update a certificate', function() { + cy.task('backendApiPut', { + token: token, + path: '/api/certificates/' + certID, + data: { + name: 'My Updated Cert' + } + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('put', 200, '/certificates/{certificateID}', data); + expect(data.result).to.have.property('id', certID); + expect(data.result).to.have.property('name', 'My Updated Cert'); + }); + }); + + it('Should be able to get all certificates', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/certificates?expand=user' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/certificates', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + expect(data.result.items[0]).to.have.property('user'); + expect(data.result.items[0].user).to.have.property('gravatar_url'); + expect(data.result.items[0].user.gravatar_url).to.include('gravatar.com'); + }); + }); + + it('Should be able to get all certificates with filters A', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/certificates?sort=name&name:starts=my&limit=1' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/certificates', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all certificates with filters B', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/certificates?id:in=1,2,3,4,5&limit=1' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/certificates', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.eq(1); + }); + }); + + it('Should be able to get all certificates with filters C', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/certificates?name:starts=xxxxxxxxxxxxxxx' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/certificates', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('total', 0); + }); + }); + + it('Should be able to delete a certificate', function() { + cy.task('backendApiDelete', { + token: token, + path: '/api/certificates/' + certID + }).then((data) => { + cy.validateSwaggerSchema('delete', 200, '/certificates/{certificateID}', data); + expect(data).to.have.property('result', true); + }); + }); + +}); diff --git a/test/cypress/e2e/api/FullCertProvision.cy.js b/test/cypress/e2e/api/FullCertProvision.cy.js new file mode 100644 index 000000000..c5cec7c4f --- /dev/null +++ b/test/cypress/e2e/api/FullCertProvision.cy.js @@ -0,0 +1,96 @@ +/// + +describe('Full Certificate Provisions', () => { + let token; + let caID; + let dnsID; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + + cy.task('backendApiPost', { + token: token, + path: '/api/certificate-authorities', + data: { + name: 'Test CA', + acmesh_server: 'https://ca.internal/acme/acme/directory', + ca_bundle: '/etc/ssl/certs/NginxProxyManager.crt', + max_domains: 5, + is_wildcard_supported: true + } + }).then((data) => { + // cy.validateSwaggerSchema('post', 201, '/certificate-authorities', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + caID = data.result.id; + }); + + cy.task('backendApiPost', { + token: token, + path: '/api/dns-providers', + data: { + acmesh_name: 'dns_pdns', + name: 'PowerDNS - example.com', + dns_sleep: 5, + meta: { + PDNS_Url: 'http://ns1.pdns:8081', + PDNS_ServerId: 'localhost', + PDNS_Token: 'npm', + PDNS_Ttl: 5 + } + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/dns-providers', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + dnsID = data.result.id; + }); + }); + }); + + it('Should be able to create new http certificate', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/certificates', + data: { + type: 'http', + certificate_authority_id: caID, + name: 'website1.example.com', + domain_names: [ + 'website1.example.com' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/certificates', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + expect(data.result.user_id).to.be.greaterThan(0); + cy.waitForCertificateStatus(token, data.result.id, 'provided'); + }); + }); + + it('Should be able to create new dns certificate', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/certificates', + data: { + type: 'dns', + certificate_authority_id: caID, + dns_provider_id: dnsID, + name: 'dns: website2.example.com', + domain_names: [ + 'website2.example.com' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/certificates', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + expect(data.result.user_id).to.be.greaterThan(0); + cy.waitForCertificateStatus(token, data.result.id, 'provided'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/Health.cy.js b/test/cypress/e2e/api/Health.cy.js new file mode 100644 index 000000000..a7053af8c --- /dev/null +++ b/test/cypress/e2e/api/Health.cy.js @@ -0,0 +1,12 @@ +/// + +describe('Basic API checks', () => { + it('Should return a valid health payload', function() { + cy.task('backendApiGet', { + path: '/api/', + }).then((data) => { + expect(data.result.healthy, 'healthy should equal true').to.equal(true); + cy.validateSwaggerSchema('get', 200, '/', data); + }); + }); +}); diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js new file mode 100644 index 000000000..0ac5bf56d --- /dev/null +++ b/test/cypress/e2e/api/Ldap.cy.js @@ -0,0 +1,65 @@ +/// + +describe('LDAP with Authentik', () => { + let token; + if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + + cy.task('backendApiPut', { + token: token, + path: '/api/settings/ldap-auth', + data: { + value: { + host: 'authentik-ldap:3389', + base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io', + user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io', + email_property: 'mail', + name_property: 'sn', + self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))', + auto_create_user: true + } + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + + cy.task('backendApiPut', { + token: token, + path: '/api/settings/auth-methods', + data: { + value: [ + 'local', + 'ldap' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + }); + + it('Should log in with LDAP', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/auth', + data: { + // Authentik LDAP creds: + type: 'ldap', + identity: 'cypress', + secret: 'fqXBfUYqHvYqiwBHWW7f' + } + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/auth', data); + expect(data.result).to.have.property('token'); + }); + }); + } +}); diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js new file mode 100644 index 000000000..912c3b64d --- /dev/null +++ b/test/cypress/e2e/api/OAuth.cy.js @@ -0,0 +1,98 @@ +/// + +describe('OAuth with Authentik', () => { + let token; + if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + + cy.task('backendApiPut', { + token: token, + path: '/api/settings/oauth-auth', + data: { + value: { + client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU', + client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq', + authorization_url: 'http://authentik:9000/application/o/authorize/', + resource_url: 'http://authentik:9000/application/o/userinfo/', + token_url: 'http://authentik:9000/application/o/token/', + logout_url: 'http://authentik:9000/application/o/npm/end-session/', + identifier: 'preferred_username', + scopes: [], + auto_create_user: true + } + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + + cy.task('backendApiPut', { + token: token, + path: '/api/settings/auth-methods', + data: { + value: [ + 'local', + 'oauth' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + }); + + it('Should log in with OAuth', function() { + cy.task('backendApiGet', { + path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), + }).then((data) => { + expect(data).to.have.property('result'); + + cy.origin('http://authentik:9000', {args: data.result}, (url) => { + cy.visit(url); + cy.get('ak-flow-executor') + .shadow() + .find('ak-stage-identification') + .shadow() + .find('input[name="uidField"]', { visible: true }) + .type('cypress'); + + cy.get('ak-flow-executor') + .shadow() + .find('ak-stage-identification') + .shadow() + .find('button[type="submit"]', { visible: true }) + .click(); + + cy.get('ak-flow-executor') + .shadow() + .find('ak-stage-password') + .shadow() + .find('input[name="password"]', { visible: true }) + .type('fqXBfUYqHvYqiwBHWW7f'); + + cy.get('ak-flow-executor') + .shadow() + .find('ak-stage-password') + .shadow() + .find('button[type="submit"]', { visible: true }) + .click(); + }) + + // we should be logged in + cy.get('#root p.chakra-text') + .first() + .should('have.text', 'Nginx Proxy Manager'); + + // logout: + cy.clearLocalStorage(); + }); + }); + } +}); diff --git a/test/cypress/e2e/api/Settings.cy.js b/test/cypress/e2e/api/Settings.cy.js new file mode 100644 index 000000000..3b1bafd2a --- /dev/null +++ b/test/cypress/e2e/api/Settings.cy.js @@ -0,0 +1,103 @@ +/// + +// Settings are stored lowercase in the backend + +describe('Settings endpoints', () => { + let token; + let settingName; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + cy.randomString(12).then((str) => { + settingName = 'cypressSetting_' + str; + settingName = settingName.trim(); + }); + }); + + it('Should be able to create new setting', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/settings', + data: { + name: settingName, + value: { + type: 'custom', + html: '

not found

' + } + } + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('post', 201, '/settings', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + + it('Should be able to get a setting', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/settings/' + settingName + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result).to.have.property('name', settingName.toLowerCase()); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + + it('Should be able to update a setting', function() { + cy.task('backendApiPut', { + token: token, + path: '/api/settings/' + settingName, + data: { + value: true + } + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); + expect(data.result).to.have.property('id'); + expect(data.result).to.have.property('name', settingName.toLowerCase()); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all settings', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/settings' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/settings', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all settings with filters A', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/settings?id:in=1,2,3,4,5&limit=1' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/settings', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all settings with filters B', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/settings?name:starts=xxxxxxxxxxxxxxx' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/settings', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('total', 0); + }); + }); + +}); diff --git a/test/cypress/e2e/api/SetupPhase.cy.js b/test/cypress/e2e/api/SetupPhase.cy.js new file mode 100644 index 000000000..010c65550 --- /dev/null +++ b/test/cypress/e2e/api/SetupPhase.cy.js @@ -0,0 +1,25 @@ +/// + +describe('Setup Phase', () => { + + before(() => { + cy.resetUsers(); + }); + + it('Should NOT be able to get a token', function() { + cy.task('backendApiPost', { + path: '/api/auth', + data: { + type: 'local', + identity: 'cypress@example.com', + secret: 'changeme' + }, + returnOnError: true + }).then((data) => { + cy.validateSwaggerSchema('post', 403, '/auth', data); + expect(data.error).to.have.property('code', 403); + expect(data.error).to.have.property('message', 'Not available during setup phase'); + }); + }); + +}); diff --git a/test/cypress/e2e/api/SwaggerSchema.cy.js b/test/cypress/e2e/api/SwaggerSchema.cy.js new file mode 100644 index 000000000..0a6847601 --- /dev/null +++ b/test/cypress/e2e/api/SwaggerSchema.cy.js @@ -0,0 +1,9 @@ +/// + +describe('Swagger Schema Checks', () => { + it('Should be valid with swagger-validator', function() { + cy.task('validateSwaggerFile', { + file: Cypress.env('swaggerBase') + }).should('equal', null); + }); +}); diff --git a/test/cypress/e2e/api/Upstreams.cy.js b/test/cypress/e2e/api/Upstreams.cy.js new file mode 100644 index 000000000..b86cd6d84 --- /dev/null +++ b/test/cypress/e2e/api/Upstreams.cy.js @@ -0,0 +1,68 @@ +/// + +describe('Upstream endpoints', () => { + let token; + let upstreamId; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + }); + + it('Should be able to create new Upstream', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/upstreams', + data: { + nginx_template_id: 5, + name: 'CypressGeneratedUpstreamA', + // These servers must be reachable by the Cypress CI stack! + servers: [ + { + + server: 'fullstack:80', + weight: 100 + }, + { + server: 'fullstack:443', + weight: 50 + } + ] + } + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('post', 201, '/upstreams', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + upstreamId = data.result.id; + }); + }); + + it('Should be able to get a Upstream', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/upstreams/' + upstreamId + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/upstreams/{upstreamID}', data); + expect(data.result).to.have.property('id'); + expect(data.result).to.have.property('name', 'CypressGeneratedUpstreamA'); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all Upstreams', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/upstreams' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/upstreams', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + +}); diff --git a/test/cypress/e2e/api/Users.cy.js b/test/cypress/e2e/api/Users.cy.js new file mode 100644 index 000000000..6a12fed14 --- /dev/null +++ b/test/cypress/e2e/api/Users.cy.js @@ -0,0 +1,189 @@ +/// + +describe('Users endpoints', () => { + let token; + let uniqueEmail; + let myUserID = 0; + + before(() => { + cy.resetUsers(); + cy.getToken().then((tok) => { + token = tok; + }); + cy.randomString(10).then((str) => { + uniqueEmail = 'jc_' + str + '@example.com'; + uniqueEmail = uniqueEmail.trim(); + }); + }); + + it('Should be able to get yourself', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/users/me' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users/{userID}', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + myUserID = data.result.id; + }); + }); + + it('Should be able to get all users', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/users' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all users with filters A', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/users?sort=created_at.desc&name:starts=c&name:ends=e' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all users with filters B', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/users?sort=created_at&id:in=1,2,3,4,5' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('items'); + expect(data.result.items.length).to.be.greaterThan(0); + }); + }); + + it('Should be able to get all users with filters C', function() { + cy.task('backendApiGet', { + token: token, + path: '/api/users?sort=name.asc&name:ends=xxxxxxxxxxxxx' + }).then((data) => { + cy.validateSwaggerSchema('get', 200, '/users', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('total', 0); + }); + }); + + it('Should be able to create someone else', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/users', + data: { + name: 'Example user 1', + email: uniqueEmail, + is_disabled: false, + auth: { + type: 'local', + secret: 'changeme' + }, + capabilities: [ + 'hosts.manage' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/users', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + }); + }); + + it('Should not be able to create duplicate email', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/users', + returnOnError: true, + data: { + name: 'Example user 2', + email: uniqueEmail, + is_disabled: false, + auth: { + type: 'local', + secret: 'changeme' + }, + capabilities: [ + 'hosts.manage' + ] + } + }).then((data) => { + cy.validateSwaggerSchema('post', 400, '/users', data); + expect(data).to.have.property('result', null); + expect(data).to.have.property('error'); + expect(data.error).to.have.property('code', 400); + }); + }); + + it('Should be able to update yourself', function() { + cy.task('backendApiPut', { + token: token, + path: '/api/users/me', + data: { + name: 'changed name' + } + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/users/{userID}', data); + expect(data).to.have.property('result'); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + expect(data.result.name).to.be.equal('changed name'); + }); + }); + + it('Should not be able to update email to another user', function() { + cy.task('backendApiPut', { + token: token, + path: '/api/users/me', + returnOnError: true, + data: { + email: uniqueEmail + } + }).then((data) => { + cy.validateSwaggerSchema('put', 400, '/users/{userID}', data); + expect(data).to.have.property('result', null); + expect(data).to.have.property('error'); + expect(data.error).to.have.property('code', 400); + }); + }); + + it('Should not be able to disable yourself', function() { + cy.task('backendApiPut', { + token: token, + path: '/api/users/me', + returnOnError: true, + data: { + is_disabled: true + } + }).then((data) => { + cy.validateSwaggerSchema('put', 400, '/users/{userID}', data); + expect(data).to.have.property('result', null); + expect(data).to.have.property('error'); + expect(data.error).to.have.property('code', 400); + }); + }); + + it('Should not be able to delete yourself', function() { + cy.task('backendApiDelete', { + token: token, + path: '/api/users/' + myUserID, + returnOnError: true + }).then((data) => { + cy.validateSwaggerSchema('delete', 400, '/users/{userID}', data); + expect(data).to.have.property('result', null); + expect(data).to.have.property('error'); + expect(data.error).to.have.property('code', 400); + }); + }); + +}); diff --git a/test/cypress/e2e/ui/SetupLogin.cy.js b/test/cypress/e2e/ui/SetupLogin.cy.js new file mode 100644 index 000000000..bd8cd50f0 --- /dev/null +++ b/test/cypress/e2e/ui/SetupLogin.cy.js @@ -0,0 +1,44 @@ +/// + +describe('UI Setup and Login', () => { + + // Clear the users before runing this test + before(() => { + cy.resetUsers(); + }); + + it('Should be able to setup a new user', function() { + cy.visit('/'); + cy.get('input[name="name"]').type('Cypress McGee'); + cy.get('input[name="email"]').type('cypress@example.com'); + cy.get('input[name="password"]').type('changeme'); + cy.get('form button:last').click(); + + cy.get('#root p.chakra-text') + .first() + .should('have.text', 'Nginx Proxy Manager'); + + // logout: + cy.clearLocalStorage(); + }); + + it('Should be able to login and change password', function() { + cy.visit('/'); + cy.get('input[name="email"]').type('cypress@example.com'); + cy.get('input[name="password"]').type('changeme'); + cy.get('form button:last').click(); + + // change password + cy.get('button[data-testid="profile-menu"]').should('be.visible').click(); + cy.get('button[data-testid="profile-menu-change-password"]').should('be.visible').click(); + cy.get('input[name="current"]').type('changeme'); + cy.get('input[name="password"]').type('ihavebeenchanged'); + cy.get('input[name="password2"]').type('ihavebeenchanged'); + cy.get('form button[data-testid="save"]').click(); + cy.get('form[data-testid="change-password"]').should('not.exist'); + + // logout: + cy.clearLocalStorage(); + }); + +}); diff --git a/test/cypress/fixtures/example.json b/test/cypress/fixtures/example.json deleted file mode 100644 index da18d9352..000000000 --- a/test/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/test/cypress/integration/api/Health.spec.js b/test/cypress/integration/api/Health.spec.js deleted file mode 100644 index 5538916cb..000000000 --- a/test/cypress/integration/api/Health.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/// - -describe('Basic API checks', () => { - it('Should return a valid health payload', function () { - cy.task('backendApiGet', { - path: '/api/', - }).then((data) => { - // Check the swagger schema: - cy.validateSwaggerSchema('get', 200, '/', data); - }); - }); - - it('Should return a valid schema payload', function () { - cy.task('backendApiGet', { - path: '/api/schema', - }).then((data) => { - expect(data.openapi).to.be.equal('3.0.0'); - }); - }); -}); diff --git a/test/cypress/integration/api/Users.spec.js b/test/cypress/integration/api/Users.spec.js deleted file mode 100644 index 43303d431..000000000 --- a/test/cypress/integration/api/Users.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -/// - -describe('Users endpoints', () => { - let token; - - before(() => { - cy.getToken().then((tok) => { - token = tok; - }); - }); - - it('Should be able to get yourself', function() { - cy.task('backendApiGet', { - token: token, - path: '/api/users/me' - }).then((data) => { - cy.validateSwaggerSchema('get', 200, '/users/{userID}', data); - expect(data).to.have.property('id'); - expect(data.id).to.be.greaterThan(0); - }); - }); - - it('Should be able to get all users', function() { - cy.task('backendApiGet', { - token: token, - path: '/api/users' - }).then((data) => { - cy.validateSwaggerSchema('get', 200, '/users', data); - expect(data.length).to.be.greaterThan(0); - }); - }); - - it('Should be able to update yourself', function() { - cy.task('backendApiPut', { - token: token, - path: '/api/users/me', - data: { - name: 'changed name' - } - }).then((data) => { - cy.validateSwaggerSchema('put', 200, '/users/{userID}', data); - expect(data).to.have.property('id'); - expect(data.id).to.be.greaterThan(0); - expect(data.name).to.be.equal('changed name'); - }); - }); - -}); diff --git a/test/cypress/plugins/backendApi/client.js b/test/cypress/plugins/backendApi/client.js index 4de398186..411b04712 100644 --- a/test/cypress/plugins/backendApi/client.js +++ b/test/cypress/plugins/backendApi/client.js @@ -19,22 +19,17 @@ BackendApi.prototype.setToken = function(token) { * @returns {Promise} */ BackendApi.prototype.get = function(path, returnOnError) { + logger('GET Request:', this.config.baseUrl + path); return new Promise((resolve, reject) => { - let headers = { - Accept: 'application/json' - }; - if (this.token) { - headers.Authorization = 'Bearer ' + this.token; - } - - logger('GET ', this.config.baseUrl + path); - restler .get(this.config.baseUrl + path, { - headers: headers, + headers: { + Accept: 'application/json', + Authorization: 'Bearer ' + this.token, + }, }) .on('complete', function(data, response) { - logger('Response data:', data); + logger('Response data:', JSON.stringify(data, null, 2)); if (!returnOnError && data instanceof Error) { reject(data); } else if (!returnOnError && response.statusCode != 200) { @@ -56,22 +51,17 @@ BackendApi.prototype.get = function(path, returnOnError) { * @returns {Promise} */ BackendApi.prototype.delete = function(path, returnOnError) { + logger('DELETE Request:', this.config.baseUrl + path); return new Promise((resolve, reject) => { - let headers = { - Accept: 'application/json' - }; - if (this.token) { - headers.Authorization = 'Bearer ' + this.token; - } - - logger('DELETE ', this.config.baseUrl + path); - restler .del(this.config.baseUrl + path, { - headers: headers, + headers: { + Accept: 'application/json', + Authorization: 'Bearer ' + this.token, + }, }) .on('complete', function(data, response) { - logger('Response data:', data); + logger('Response data:', JSON.stringify(data, null, 2)); if (!returnOnError && data instanceof Error) { reject(data); } else if (!returnOnError && response.statusCode != 200) { @@ -94,7 +84,7 @@ BackendApi.prototype.delete = function(path, returnOnError) { * @returns {Promise} */ BackendApi.prototype.postJson = function(path, data, returnOnError) { - logger('POST ', this.config.baseUrl + path); + logger('POST Request:', this.config.baseUrl + path, JSON.stringify(data, null, 2)); return this._putPostJson('postJson', path, data, returnOnError); }; @@ -105,7 +95,7 @@ BackendApi.prototype.postJson = function(path, data, returnOnError) { * @returns {Promise} */ BackendApi.prototype.putJson = function(path, data, returnOnError) { - logger('PUT ', this.config.baseUrl + path); + logger('PUT Request:', this.config.baseUrl + path, JSON.stringify(data, null, 2)); return this._putPostJson('putJson', path, data, returnOnError); }; @@ -123,7 +113,7 @@ BackendApi.prototype._putPostJson = function(fn, path, data, returnOnError) { Authorization: 'Bearer ' + this.token, }, }).on('complete', function(data, response) { - logger('Response data:', data); + logger('Response data:', JSON.stringify(data, null, 2)); if (!returnOnError && data instanceof Error) { reject(data); } else if (!returnOnError && response.statusCode != 200) { diff --git a/test/cypress/plugins/backendApi/task.js b/test/cypress/plugins/backendApi/task.js index 2f67902d5..6ba5c946f 100644 --- a/test/cypress/plugins/backendApi/task.js +++ b/test/cypress/plugins/backendApi/task.js @@ -9,8 +9,8 @@ module.exports = function (config) { /** * @param {object} options + * @param {string} options.token JWT * @param {string} options.path API path - * @param {string} [options.token] JWT * @param {bool} [options.returnOnError] If true, will return instead of throwing errors * @returns {string} */ diff --git a/test/cypress/plugins/index.js b/test/cypress/plugins/index.js index 8cf6eef6c..c02c563da 100644 --- a/test/cypress/plugins/index.js +++ b/test/cypress/plugins/index.js @@ -1,7 +1,8 @@ const {SwaggerValidation} = require('@jc21/cypress-swagger-validation'); +const chalk = require('chalk'); module.exports = (on, config) => { - // Replace swaggerBase config var wildcard + // Replace swaggerFile config var wildcard if (typeof config.env.swaggerBase !== 'undefined') { config.env.swaggerBase = config.env.swaggerBase.replace('{{baseUrl}}', config.baseUrl); } @@ -11,7 +12,7 @@ module.exports = (on, config) => { on('task', require('./backendApi/task')(config)); on('task', { log(message) { - console.log(message); + console.log(chalk.cyan.bold('[') + chalk.blue.bold('LOG') + chalk.cyan.bold(']') + ' ' + chalk.red.bold(message)); return null; } }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index ea4af029b..00ea5ecd1 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -9,34 +9,132 @@ // *********************************************** // +import 'cypress-wait-until'; + +Cypress.Commands.add('randomString', (length) => { + var result = ''; + var characters = 'ABCDEFGHIJK LMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}); + /** * Check the swagger schema: * * @param {string} method API Method in swagger doc, "get", "put", "post", "delete" - * @param {number} statusCode API status code in swagger doc + * @param {integer} code Swagger doc endpoint response code, exactly as defined in swagger doc * @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc * @param {*} data The API response data to check against the swagger schema */ -Cypress.Commands.add('validateSwaggerSchema', (method, statusCode, path, data) => { +Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => { cy.task('validateSwaggerSchema', { file: Cypress.env('swaggerBase'), endpoint: path, method: method, - statusCode: statusCode, + statusCode: code, responseSchema: data, verbose: true }).should('equal', null); }); -Cypress.Commands.add('getToken', () => { - // login with existing user - cy.task('backendApiPost', { - path: '/api/tokens', - data: { - identity: 'admin@example.com', - secret: 'changeme' +/** + * @param {object} defaultUser + * @param {object} defaultAuth +*/ +Cypress.Commands.add('getToken', (defaultUser, defaultAuth) => { + if (typeof defaultAuth === 'object' && defaultAuth) { + if (!defaultUser) { + defaultUser = {}; } - }).then(res => { - cy.wrap(res.token); + defaultUser.auth = defaultAuth; + } + + cy.task('backendApiGet', { + path: '/api/', + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('get', 200, '/', data); + + if (!data.result.setup) { + cy.log('Setup = false'); + // create a new user + cy.createInitialUser(defaultUser).then(() => { + return cy.getToken(defaultUser); + }); + } else { + let auth = { + type: 'local', + identity: 'cypress@example.com', + secret: 'changeme', + }; + + if (typeof defaultUser === 'object' && defaultUser && typeof defaultUser.auth === 'object' && defaultUser.auth) { + auth = Object.assign({}, auth, defaultUser.auth); + } + + cy.log('Setup = true'); + // login with existing user + cy.task('backendApiPost', { + path: '/api/auth', + data: auth, + }).then((res) => { + cy.wrap(res.result.token); + }); + } + }); +}); + +Cypress.Commands.add('createInitialUser', (defaultUser) => { + let user = { + name: 'Cypress McGee', + email: 'cypress@example.com', + is_disabled: false, + auth: { + type: 'local', + secret: 'changeme' + }, + capabilities: ['full-admin'] + }; + + if (typeof defaultUser === 'object' && defaultUser) { + user = Object.assign({}, user, defaultUser); + } + + return cy.task('backendApiPost', { + path: '/api/users', + data: user, + }).then((data) => { + // Check the swagger schema: + cy.validateSwaggerSchema('post', 201, '/users', data); + expect(data.result).to.have.property('id'); + expect(data.result.id).to.be.greaterThan(0); + cy.wrap(data.result); + }); +}); + +Cypress.Commands.add('resetUsers', () => { + cy.task('backendApiDelete', { + path: '/api/users' + }).then((data) => { + expect(data).to.have.property('result', true); + cy.wrap(data.result); + }); +}); + +Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeout = 60) => { + cy.log(`Waiting for certificate (${certID}) status (${expected}) timeout (${timeout})`); + + cy.waitUntil(() => cy.task('backendApiGet', { + token: token, + path: `/api/certificates/${certID}` + }).then((data) => { + return data.result.status === expected; + }), { + errorMsg: 'Waiting for certificate status failed', + timeout: timeout * 1000, + interval: 5000 }); }); diff --git a/test/cypress/support/e2e.js b/test/cypress/support/e2e.js new file mode 100644 index 000000000..9f724bf41 --- /dev/null +++ b/test/cypress/support/e2e.js @@ -0,0 +1,7 @@ +import './commands'; + +Cypress.on('uncaught:exception', (/*err, runnable*/) => { + // returning false here prevents Cypress from + // failing the test + return false; +}); diff --git a/test/cypress/support/index.js b/test/cypress/support/index.js deleted file mode 100644 index 4fc9aafec..000000000 --- a/test/cypress/support/index.js +++ /dev/null @@ -1,9 +0,0 @@ -require('cypress-plugin-retries'); - -import './commands'; - -Cypress.on('uncaught:exception', (/*err, runnable*/) => { - // returning false here prevents Cypress from - // failing the test - return false; -}); diff --git a/test/package.json b/test/package.json index 781597b33..ef9bc2671 100644 --- a/test/package.json +++ b/test/package.json @@ -1,26 +1,26 @@ { - "name": "test", + "name": "npmtestsuite", "version": "1.0.0", "description": "", "main": "index.js", "dependencies": { - "@jc21/cypress-swagger-validation": "^0.0.9", + "@jc21/cypress-swagger-validation": "^0.3.1", "@jc21/restler": "^3.4.0", "chalk": "^4.1.0", - "cypress": "^5.6.0", - "cypress-multi-reporters": "^1.4.0", - "cypress-plugin-retries": "^1.5.2", - "eslint": "^7.6.0", + "cypress": "^13.15.1", + "cypress-multi-reporters": "^2.0.4", + "cypress-wait-until": "^3.0.2", + "eslint": "^9.14.0", "eslint-plugin-align-assignments": "^1.1.2", - "eslint-plugin-chai-friendly": "^0.6.0", - "eslint-plugin-cypress": "^2.11.1", - "lodash": "^4.17.19", - "mocha": "^8.1.1", - "mocha-junit-reporter": "^2.0.0" + "eslint-plugin-chai-friendly": "^1.0.1", + "eslint-plugin-cypress": "^4.1.0", + "lodash": "^4.17.21", + "mocha": "^10.8.2", + "mocha-junit-reporter": "^2.2.1" }, "scripts": { - "cypress": "cypress open --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}", - "cypress:headless": "cypress run --config-file=cypress/config/dev.json --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}" + "cypress": "cypress open --config-file=cypress/config/dev.js --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}", + "cypress:headless": "cypress run --config-file=cypress/config/dev.js --config baseUrl=${BASE_URL:-http://127.0.0.1:3081}" }, "author": "", "license": "ISC" diff --git a/test/yarn.lock b/test/yarn.lock index 0ab75f98a..d4d23da09 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -11,36 +11,42 @@ call-me-maybe "^1.0.1" js-yaml "^3.13.1" -"@babel/code-frame@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" - integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== +"@apidevtools/json-schema-ref-parser@^11.7.2": + version "11.7.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz#cdf3e0aded21492364a70e193b45b7cf4177f031" + integrity sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA== dependencies: - "@babel/highlight" "^7.0.0" + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.0" -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" +"@apidevtools/openapi-schemas@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== -"@cypress/listr-verbose-renderer@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" - integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.1.0.tgz#a987d71e5be61feb623203be0c96e5985b192ab6" + integrity sha512-9Kt7EuS/7WbMAUv2gSziqjvxwDbFSg3Xeyfuj5laUODX8o/k/CpsAKiQ8W7/R88eXFTMbJYg6+7uAmOWNKmwnw== dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" + "@apidevtools/json-schema-ref-parser" "9.0.6" + "@apidevtools/openapi-schemas" "^2.1.0" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + ajv "^8.6.3" + ajv-draft-04 "^1.0.0" + call-me-maybe "^1.0.1" -"@cypress/request@^2.88.5": - version "2.88.5" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" - integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== +"@cypress/request@^3.0.4": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.6.tgz#f5580add6acee0e183b4d4e07eff4f31327ae12b" + integrity sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -48,20 +54,18 @@ combined-stream "~1.0.6" extend "~3.0.2" forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" + form-data "~4.0.0" + http-signature "~1.4.0" is-typedarray "~1.0.0" isstream "~0.1.2" json-stringify-safe "~5.0.1" mime-types "~2.1.19" - oauth-sign "~0.9.0" performance-now "^2.1.0" - qs "~6.5.2" + qs "6.13.0" safe-buffer "^5.1.2" - tough-cookie "~2.5.0" + tough-cookie "^5.0.0" tunnel-agent "^0.6.0" - uuid "^3.3.2" + uuid "^8.3.2" "@cypress/xvfb@^1.2.4": version "1.2.4" @@ -71,17 +75,106 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@jc21/cypress-swagger-validation@^0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.0.9.tgz#8bed970d8d34e081248ddac967d69ded89f2dbd6" - integrity sha512-OkqXSGd5pFMODd4s646GmDLt39afzIvqPGIZHyXbOMDyR5CsGYOIrJk9GcDVn+RTdII0BV9Or03LVyErEImaeg== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: - ajv "^6.12.3" - chalk "^4.1.0" - json-schema "^0.2.5" - json-schema-ref-parser "^9.0.5" - jsonpath "^1.0.2" - lodash "^4.17.19" + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.18.0.tgz#37d8fe656e0d5e3dbaea7758ea56540867fd074d" + integrity sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw== + dependencies: + "@eslint/object-schema" "^2.1.4" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/core@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.7.0.tgz#a1bb4b6a4e742a5ff1894b7ee76fbf884ec72bd3" + integrity sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw== + +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.14.0": + version "9.14.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.14.0.tgz#2347a871042ebd11a00fd8c2d3d56a265ee6857e" + integrity sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg== + +"@eslint/object-schema@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.4.tgz#9e69f8bb4031e11df79e03db09f9dbbae1740843" + integrity sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ== + +"@eslint/plugin-kit@^0.2.0": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz#5eff371953bc13e3f4d88150e2c53959f64f74f6" + integrity sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw== + dependencies: + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.6" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" + integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.3.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" + integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== + +"@humanwhocodes/retry@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.0.tgz#b57438cab2a2381b4b597b0ab17339be381bd755" + integrity sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g== + +"@jc21/cypress-swagger-validation@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jc21/cypress-swagger-validation/-/cypress-swagger-validation-0.3.1.tgz#1cdd49850a20f876ed62149623f99988264751be" + integrity sha512-Vdt1gLfj8p0tJhA42Cfn43XBbsKocNfVCEVSwkn7RmZgWUyRKjqhBBRTVa9cKZTozyg8Co/yhBMsNyjmHFVXtQ== + dependencies: + "@apidevtools/json-schema-ref-parser" "^11.7.2" + "@apidevtools/swagger-parser" "^10.1.0" + ajv "^8.17.1" + axios "^1.7.7" + json-schema "^0.4.0" + jsonpath "^1.1.1" + lodash "^4.17.21" + openapi-types "^12.1.3" + picocolors "^1.1.0" "@jc21/restler@^3.4.0": version "3.4.0" @@ -98,99 +191,134 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" - integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg== - dependencies: - any-observable "^0.3.0" - "@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== -"@types/sinonjs__fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" - integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== +"@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@*": + version "15.12.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" + integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== + +"@types/sinonjs__fake-timers@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== "@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== -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== +"@types/yauzl@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" + integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== + dependencies: + "@types/node" "*" -acorn@^7.3.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== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== +acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +acorn@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" + integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-draft-04@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" + integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: - fast-deep-equal "^2.0.1" + fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.3: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== +ajv@^8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ajv@^8.6.3: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@4.1.1, ansi-colors@^4.1.1: +ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -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-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== -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-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" 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@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -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== +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - color-convert "^1.9.0" + color-convert "^2.0.1" ansi-styles@^4.1.0: version "4.2.1" @@ -200,23 +328,18 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - -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== +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" -arch@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" - integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== argparse@^1.0.7: version "1.0.10" @@ -225,15 +348,10 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -array.prototype.map@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec" - integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.4" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== asn1@~0.2.3: version "0.2.4" @@ -247,10 +365,10 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -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== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.0: version "3.2.0" @@ -273,15 +391,29 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" - integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A== + version "1.10.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" + integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== + +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" 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= +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -290,11 +422,11 @@ bcrypt-pbkdf@^1.0.0: tweetnacl "^0.14.3" 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== + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -blob-util@2.0.2: +blob-util@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== @@ -312,6 +444,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -319,7 +458,7 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" -browser-stdout@1.3.1: +browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== @@ -329,16 +468,30 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + 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" @@ -349,45 +502,25 @@ callsites@^3.0.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== +camelcase@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^1.0.0, chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.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== +chalk@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72" + integrity sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.0.0, chalk@^4.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== @@ -395,88 +528,73 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -charenc@~0.0.1: +charenc@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= -chokidar@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" - integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.3.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.2" -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== +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cli-cursor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= - dependencies: - restore-cursor "^1.0.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^2.0.0, cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= +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 "^2.0.0" + restore-cursor "^3.1.0" -cli-table3@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" - integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== +cli-table3@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== dependencies: - object-assign "^4.1.0" string-width "^4.2.0" optionalDependencies: - colors "^1.1.2" - -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= - dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" + colors "1.4.0" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.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= + slice-ansi "^3.0.0" + string-width "^4.2.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== +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: - color-name "1.1.3" + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" color-convert@^2.0.1: version "2.0.1" @@ -485,32 +603,32 @@ color-convert@^2.0.1: 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== -colors@^1.1.2: +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + +colors@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== common-tags@^1.8.0: version "1.8.0" @@ -522,17 +640,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -546,68 +654,71 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -crypt@~0.0.1: +crypt@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -cypress-multi-reporters@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-1.4.0.tgz#5f1d0484a20959cfe782f1bf65ad16c6ad804da7" - integrity sha512-CjpQduW43KVzY45hhKC/qf8MSebRpx6JyEz6py8F+0GrYS8rE5TZ8wXv9dPUs/PaT6w+dR8KIgLSMr967Om7iA== +cypress-multi-reporters@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/cypress-multi-reporters/-/cypress-multi-reporters-2.0.4.tgz#581c1b937be7bee4e3cae976d774ad86e0f8b7e7" + integrity sha512-TZKzSfo8ReU2Fuj1n90gi4Ocw1a/nh6utiq9g0wy27muq1/IjZXdR97WXkV0to2vd8NRldXt+tuKEmxQrp8LDg== dependencies: - debug "^4.1.1" - lodash "^4.17.15" + debug "^4.3.7" + lodash "^4.17.21" -cypress-plugin-retries@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.5.2.tgz#21d5247cd77013b95bbfdd914f2de66f91f76a2e" - integrity sha512-o1xVIGtv4WvNVxoVJ2X08eAuvditPHrePRzHqhwwHbMKu3C2rtxCdanRCZdO5fjh8ww+q4v4V0e9GmysbOvu3A== - dependencies: - chalk "^3.0.0" +cypress-wait-until@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz#c90dddfa4c46a2c422f5b91d486531c560bae46e" + integrity sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w== -cypress@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.6.0.tgz#6781755c3ddfd644ce3179fcd7389176c0c82280" - integrity sha512-cs5vG3E2JLldAc16+5yQxaVRLLqMVya5RlrfPWkC72S5xrlHFdw7ovxPb61s4wYweROKTyH01WQc2PFzwwVvyQ== +cypress@^13.15.1: + version "13.15.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.15.1.tgz#d85074e07cc576eb30068617d529719ef6093b69" + integrity sha512-DwUFiKXo4lef9kA0M4iEhixFqoqp2hw8igr0lTqafRb9qtU3X0XGxKbkSYsUFdkrAkphc7MPDxoNPhk5pj9PVg== dependencies: - "@cypress/listr-verbose-renderer" "^0.4.1" - "@cypress/request" "^2.88.5" + "@cypress/request" "^3.0.4" "@cypress/xvfb" "^1.2.4" - "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" - arch "^2.1.2" - blob-util "2.0.2" + arch "^2.2.0" + blob-util "^2.0.2" bluebird "^3.7.2" + buffer "^5.7.1" cachedir "^2.3.0" chalk "^4.1.0" check-more-types "^2.24.0" - cli-table3 "~0.6.0" - commander "^5.1.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.1" + commander "^6.2.1" common-tags "^1.8.0" - debug "^4.1.1" - eventemitter2 "^6.4.2" - execa "^4.0.2" + dayjs "^1.10.4" + debug "^4.3.4" + enquirer "^2.3.6" + eventemitter2 "6.4.7" + execa "4.1.0" executable "^4.1.1" - extract-zip "^1.7.0" - fs-extra "^9.0.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" getos "^3.2.1" - is-ci "^2.0.0" - is-installed-globally "^0.3.2" + is-ci "^3.0.1" + is-installed-globally "~0.4.0" lazy-ass "^1.6.0" - listr "^0.14.3" - lodash "^4.17.19" + listr2 "^3.8.3" + lodash "^4.17.21" log-symbols "^4.0.0" - minimist "^1.2.5" - moment "^2.27.0" + minimist "^1.2.8" ospath "^1.2.2" - pretty-bytes "^5.4.1" - ramda "~0.26.1" + pretty-bytes "^5.6.0" + process "^0.11.10" + proxy-from-env "1.0.0" request-progress "^3.0.0" - supports-color "^7.2.0" - tmp "~0.2.1" + semver "^7.5.3" + supports-color "^8.1.1" + tmp "~0.2.3" + tree-kill "1.2.2" untildify "^4.0.0" - url "^0.11.0" yauzl "^2.10.0" dashdash@^1.12.0: @@ -617,65 +728,74 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +dayjs@^1.10.4: + version "1.10.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986" + integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g== -debug@3.2.6, debug@^3.1.0: +debug@^3.1.0: 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" -debug@^2.2.0, debug@^2.6.9: - 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.0.1, debug@^4.1.1: +debug@^4.1.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" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +debug@^4.3.1, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@^4.3.5, debug@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== deep-is@^0.1.3, 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= -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: - object-keys "^1.0.12" + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -diff@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -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" +diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== ecc-jsbn@~0.1.1: version "0.1.2" @@ -685,16 +805,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= - -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" @@ -707,85 +817,46 @@ end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enquirer@^2.3.5: +enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: ansi-colors "^4.1.1" -es-abstract@^1.17.0-next.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0.tgz#f42a517d0036a5591dbb2c463591dc8bb50309b1" - integrity sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" - -es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-array-method-boxes-properly@^1.0.0: +es-define-property@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - -es-get-iterator@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +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= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.8.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541" - integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg== + version "1.14.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== dependencies: - esprima "^3.1.3" + esprima "^4.0.1" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" @@ -797,129 +868,135 @@ eslint-plugin-align-assignments@^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-plugin-chai-friendly@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.6.0.tgz#54052fab79302ed0cea76ab997351ea4809bfb77" - integrity sha512-Uvvv1gkbRGp/qfN15B0kQyQWg+oFA8buDSqrwmW3egNSk/FpqH2MjQqKOuKwmEL6w4QIQrIjDp+gg6kGGmD3oQ== - -eslint-plugin-cypress@^2.11.1: - version "2.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.1.tgz#a945e2774b88211e2c706a059d431e262b5c2862" - integrity sha512-MxMYoReSO5+IZMGgpBZHHSx64zYPSPTpXDwsgW7ChlJTF/sA+obqRbHplxD6sBStE+g4Mi0LCLkG4t9liu//mQ== - dependencies: - globals "^11.12.0" +eslint-plugin-chai-friendly@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-1.0.1.tgz#c3290b5294c1145934cf9c07eaa4cec87921d18c" + integrity sha512-dxD/uz1YKJ8U4yah1i+V/p/u+kHRy3YxTPe2nJGqb5lCR+ucan/KIexfZ5+q4X+tkllyMe86EBbAkdlwxNy3oQ== -eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== +eslint-plugin-cypress@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-4.1.0.tgz#11178fd250d437e2ec57bf24b8a9058b356f8cac" + integrity sha512-JhqkMY02mw74USwK9OFhectx3YSj6Co1NgWBxlGdKvlqiAp9vdEuQqt33DKGQFvvGS/NWtduuhWXWNnU29xDSg== dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" + globals "^15.11.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.2.0.tgz#377aa6f1cb5dc7592cfd0b7f892fd0cf352ce442" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + esrecurse "^4.3.0" + estraverse "^5.2.0" -eslint-visitor-keys@^1.3.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-visitor-keys@^3.3.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" + integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.6.0.tgz#522d67cfaea09724d96949c70e7a0550614d64d6" - integrity sha512-QlAManNtqr7sozWm5TF4wIH9gmUm2hE3vNRUvyoYAa4y1l5/jxD/PQStEjBMQtCqZmSep8UxrcecI60hOpe61w== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + +eslint-visitor-keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" + integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== + +eslint@^9.14.0: + version "9.14.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.14.0.tgz#534180a97c00af08bcf2b60b0ebf0c4d6c1b2c95" + integrity sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.18.0" + "@eslint/core" "^0.7.0" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.14.0" + "@eslint/plugin-kit" "^0.2.0" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.0" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - eslint-scope "^5.1.0" - eslint-utils "^2.1.0" - eslint-visitor-keys "^1.3.0" - espree "^7.2.0" - esquery "^1.2.0" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.2.0" + eslint-visitor-keys "^4.2.0" + espree "^10.3.0" + esquery "^1.5.0" 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" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash "^4.17.19" - minimatch "^3.0.4" + lodash.merge "^4.6.2" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^5.2.3" + optionator "^0.9.3" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69" - integrity sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g== +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== + dependencies: + acorn "^8.11.3" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + +espree@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.3.0.tgz#29267cf5b0cb98735b65e64ba07e0ed49d1eed8a" + integrity sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg== dependencies: - acorn "^7.3.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.3.0" + acorn "^8.14.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.0" esprima@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" integrity sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs= -esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= - -esprima@^4.0.0: +esprima@^4.0.0, esprima@^4.0.1: 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.2.0: - 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== +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== 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== +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== dependencies: - estraverse "^4.1.0" + estraverse "^5.2.0" -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.2.0: 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.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + +estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== @@ -929,12 +1006,12 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter2@^6.4.2: - version "6.4.3" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" - integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== +eventemitter2@6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== -execa@^4.0.2: +execa@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -956,25 +1033,21 @@ executable@^4.1.1: dependencies: pify "^2.2.0" -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= - extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extract-zip@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" + debug "^4.1.1" + get-stream "^5.1.0" yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" extsprintf@1.3.0: version "1.3.0" @@ -986,12 +1059,12 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-deep-equal@^3.1.3: 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== @@ -1006,6 +1079,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-uri@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" + integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -1013,27 +1091,19 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -figures@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.2.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== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^2.0.1" + flat-cache "^4.0.0" fill-range@^7.0.1: version "7.0.1" @@ -1042,57 +1112,52 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -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== +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^5.0.0" + locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: - locate-path "^3.0.0" + flatted "^3.2.9" + keyv "^4.5.4" -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" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -flat@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" - integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== - dependencies: - is-buffer "~2.0.3" +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== +form-data@^4.0.0, form-data@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" + integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== dependencies: asynckit "^0.4.0" - combined-stream "^1.0.6" + combined-stream "^1.0.8" mime-types "^2.1.12" -fs-extra@^9.0.1: +fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -1107,27 +1172,33 @@ fs.realpath@^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== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -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= +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -get-caller-file@^2.0.1: +get-caller-file@^2.0.5: 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@^5.0.0: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== @@ -1148,109 +1219,102 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.0: +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@7.1.6, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^5.0.1" 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" - -globals@^11.12.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" - integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== dependencies: - type-fest "^0.8.1" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + ini "2.0.0" -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -har-validator@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" +globals@^15.11.0: + version "15.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.11.0.tgz#b96ed4c6998540c6fb824b24b5499216d2438d6e" + integrity sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw== -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== dependencies: - ansi-regex "^2.0.0" + get-intrinsic "^1.1.3" -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= +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== 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-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== -has@^1.0.3: +has-symbols@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" -he@1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +http-signature@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" + integrity sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg== dependencies: assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + jsprim "^2.0.2" + sshpk "^1.18.0" human-signals@^1.1.1: version "1.1.1" @@ -1262,15 +1326,20 @@ iconv-lite@0.2.11: resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" integrity sha1-HOYKOleGSiktEyH/RgnKS7llrcg= -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== +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -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== +ignore@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -1280,10 +1349,10 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== inflight@^1.0.4: version "1.0.6" @@ -1293,20 +1362,15 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== is-binary-path@~2.1.0: version "2.1.0" @@ -1315,55 +1379,23 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@~1.1.1: +is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - -is-callable@^1.1.4, is-callable@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" - integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== - -is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - -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== +is-ci@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== dependencies: - ci-info "^2.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + ci-info "^3.2.0" 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" @@ -1376,87 +1408,41 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-installed-globally@^0.3.2: - 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== +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" + is-extglob "^2.1.1" -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^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-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -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-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-promise@^2.1.0: +is-plain-obj@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - -is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== - dependencies: - has "^1.0.3" - -is-regex@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 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-string@^1.0.4, is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1467,16 +1453,6 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -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" @@ -1487,58 +1463,45 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -iterate-iterator@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" - integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw== - -iterate-value@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" - integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== - dependencies: - es-get-iterator "^1.0.2" - iterate-iterator "^1.0.1" - -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, js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== +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" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -json-schema-ref-parser@^9.0.5: - version "9.0.6" - resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#fc89a5e6b853f2abe8c0af30d3874196526adb60" - integrity sha512-z0JGv7rRD3CnJbZY/qCpscyArdtLJhr/wRBmFUdoZ8xMjsFyNdILSprG2degqRLjBjyhZHAEBpGOxniO9rKTxA== - dependencies: - "@apidevtools/json-schema-ref-parser" "9.0.6" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 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-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" - integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ== +json-schema@0.4.0, json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" @@ -1559,25 +1522,32 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonpath@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.0.2.tgz#e6aae681d03e9a77b4651d5d96eac5fc63b1fd13" - integrity sha512-rmzlgFZiQPc6q4HDyK8s9Qb4oxBnI5sF61y/Co5PV0lc3q2bIuRsNdueVbhoSHdKM4fxeimphOAtfz47yjCfeA== +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== dependencies: esprima "1.2.2" static-eval "2.0.2" - underscore "1.7.0" + underscore "1.12.1" -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -1599,90 +1569,49 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== - dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== +listr2@^3.8.3: + version "3.10.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f" + integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw== dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.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== +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== dependencies: - p-locate "^4.1.0" + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== lodash.once@^4.1.1: 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== -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" - -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== dependencies: - chalk "^1.0.0" + chalk "^4.0.0" -log-symbols@^4.0.0: +log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -1690,139 +1619,118 @@ log-symbols@^4.0.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -log-update@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: - ansi-escapes "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -mime-db@1.42.0: - version "1.42.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" - integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== +mime-db@1.44.0: + 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.12, mime-types@~2.1.19: - version "2.1.25" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" - integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== + 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.42.0" - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + mime-db "1.44.0" 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== -minimatch@3.0.4, 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== +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -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== - -mkdirp@^0.5.1, mkdirp@~0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= +minimatch@^5.0.1, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: - minimist "0.0.8" + brace-expansion "^2.0.1" -mkdirp@^0.5.4: - 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" +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -mocha-junit-reporter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.0.0.tgz#3bf990fce7a42c0d2b718f188553a25d9f24b9a2" - integrity sha512-20HoWh2HEfhqmigfXOKUhZQyX23JImskc37ZOhIjBKoBEsb+4cAFRJpAVhFpnvsztLklW/gFVzsrobjLwmX4lA== - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" +mkdirp@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== -mocha@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.1.tgz#1de1ba4e9a2c955d96b84e469d7540848223592d" - integrity sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ== - dependencies: - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.3.1" - debug "3.2.6" - diff "4.0.2" - escape-string-regexp "1.0.5" - find-up "4.1.0" - glob "7.1.6" - growl "1.10.5" - he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" - minimatch "3.0.4" - ms "2.1.2" - object.assign "4.1.0" - promise.allsettled "1.0.2" - serialize-javascript "4.0.0" - strip-json-comments "3.0.1" - supports-color "7.1.0" - which "2.0.2" - wide-align "1.1.3" - workerpool "6.0.0" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.1" - -moment@^2.27.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= +mocha-junit-reporter@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz#739f5595d0f051d07af9d74e32c416e13a41cde5" + integrity sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw== + dependencies: + debug "^4.3.4" + md5 "^2.3.0" + mkdirp "^3.0.0" + strip-ansi "^6.0.1" + xml "^1.0.1" + +mocha@^10.8.2: + version "10.8.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.8.2.tgz#8d8342d016ed411b12a429eb731b825f961afb96" + integrity sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" ms@2.1.2, 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== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -1840,40 +1748,10 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.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= - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -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-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@4.1.0, object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -1882,18 +1760,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -1901,6 +1767,11 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +openapi-types@^12.1.3: + version "12.1.3" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -1913,60 +1784,43 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" + word-wrap "^1.2.5" ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= -p-limit@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" - integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== - dependencies: - p-try "^2.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== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-try "^2.0.0" + yocto-queue "^0.1.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== dependencies: - p-limit "^2.0.0" + p-limit "^3.0.2" -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== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: - p-limit "^2.2.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -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== + aggregate-error "^3.0.0" parent-module@^1.0.0: version "1.0.1" @@ -1975,21 +1829,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -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@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -2005,11 +1849,21 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.7: +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2025,36 +1879,25 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -pretty-bytes@^5.4.1: +pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -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== - -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== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== -promise.allsettled@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" - integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== - dependencies: - array.prototype.map "^1.0.1" - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - iterate-value "^1.0.0" +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== pump@^3.0.0: version "3.0.0" @@ -2064,12 +1907,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^2.1.0, punycode@^2.1.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== @@ -2079,20 +1917,12 @@ qs@1.2.0: resolved "https://registry.yarnpkg.com/qs/-/qs-1.2.0.tgz#ed079be28682147e6fd9a34cc2b0c1e0ec6453ee" integrity sha1-7Qeb4oaCFH5v2aNMwrDB4OxkU+4= -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -ramda@~0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" randombytes@^2.1.0: version "2.1.0" @@ -2101,30 +1931,12 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -readable-stream@^2.2.2: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== - 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.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" - integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: - picomatch "^2.0.7" - -regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + picomatch "^2.2.1" request-progress@^3.0.0: version "3.0.0" @@ -2138,68 +1950,36 @@ require-directory@^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== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== 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== -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= +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 "^2.0.0" + onetime "^5.1.0" signal-exit "^3.0.2" -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@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rxjs@^6.3.3: - version "6.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" - integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safe-buffer@^5.1.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, 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-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== - safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -2210,22 +1990,29 @@ sax@0.5.x: resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= -semver@^7.2.1: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@^7.5.3: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -serialize-javascript@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.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-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" shebang-command@^2.0.0: version "2.0.0" @@ -2239,24 +2026,38 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.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== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" source-map@~0.6.1: version "0.6.1" @@ -2268,10 +2069,10 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== +sshpk@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -2290,33 +2091,7 @@ static-eval@2.0.2: dependencies: escodegen "^1.8.1" -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", string-width@^2.1.1: - 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, string-width@^3.1.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.2.0: +string-width@^4.1.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== @@ -2325,65 +2100,14 @@ string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimleft@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" - integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" - integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -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.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== +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: - ansi-regex "^4.1.0" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" strip-ansi@^6.0.0: version "6.0.0" @@ -2392,62 +2116,37 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== - -strip-json-comments@^3.1.0: +strip-json-comments@^3.1.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== -supports-color@7.1.0, supports-color@^7.1.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" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.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.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" -symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - -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" - text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2458,12 +2157,27 @@ throttleit@^1.0.0: resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= -tmp@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tldts-core@^6.1.58: + version "6.1.58" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.58.tgz#f0b5c1fcb2e214f558c7cb380fb1e6f4b2459d8b" + integrity sha512-dR936xmhBm7AeqHIhCWwK765gZ7dFyL+IqLSFAjJbFlUXGMLCb8i2PzlzaOuWBuplBTaBYseSb565nk/ZEM0Bg== + +tldts@^6.1.32: + version "6.1.58" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.58.tgz#63d211f46f2c17d69d4cedf0c4c19423a608874f" + integrity sha512-MQJrJhjHOYGYb8DobR6Y4AdDbd4TYkyQ+KBDVc5ODzs1cbrvPpfN1IemYi9jfipJ/vR1YWvrDli0hg1y19VRoA== dependencies: - rimraf "^3.0.0" + tldts-core "^6.1.58" + +tmp@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== to-regex-range@^5.0.1: version "5.0.1" @@ -2472,18 +2186,22 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== dependencies: - psl "^1.1.28" - punycode "^2.1.1" + tldts "^6.1.32" + +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== tslib@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== tunnel-agent@^0.6.0: version "0.6.0" @@ -2511,20 +2229,15 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -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-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -underscore@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" - integrity sha1-a7rwh3UA02vjTsqlhODbn+8DUgk= +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== universalify@^2.0.0: version "2.0.0" @@ -2543,28 +2256,10 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -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= - -uuid@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - -v8-compile-cache@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" - integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== verror@1.10.0: version "1.10.0" @@ -2575,64 +2270,51 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -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@2.0.2, which@^2.0.1: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@1.1.3: - 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" +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -word-wrap@^1.2.3, word-wrap@~1.2.3: +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== -workerpool@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" - integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== +workerpool@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" + integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= +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: - string-width "^2.1.1" - strip-ansi "^4.0.0" + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" + 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@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" - xml2js@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.0.tgz#124fc4114b4129c810800ecb2ac86cf25462cb9a" @@ -2641,85 +2323,58 @@ xml2js@0.4.0: sax "0.5.x" xmlbuilder ">=0.4.2" -xml@^1.0.0: +xml@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== xmlbuilder@>=0.4.2: - version "13.0.2" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-13.0.2.tgz#02ae33614b6a047d1c32b5389c1fdacb2bce47a7" - integrity sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ== + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== -y18n@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 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@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-unparser@1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" - integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== - dependencies: - camelcase "^5.3.1" - decamelize "^1.2.0" - flat "^4.1.0" - is-plain-obj "^1.1.0" - yargs "^14.2.3" - -yargs@13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.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 "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^14.2.3: - version "14.2.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" +yargs-parser@^20.2.2: + version "20.2.7" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" + integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + +yargs-parser@^20.2.9: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^15.0.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yauzl@^2.10.0: version "2.10.0" @@ -2728,3 +2383,8 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==