diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 313497cf9..000000000 --- a/.babelrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presets": [ - ["env", { - "targets": { - "browsers": ["Chrome >= 65"] - }, - "debug": false, - "modules": false, - "useBuiltIns": "usage" - }] - ] -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 9860702e0..6ec6a7c91 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,15 +6,30 @@ labels: bug assignees: '' --- + **Checklist** - Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image? + - Yes / No - Are you sure you're not using someone else's docker image? -- If having problems with Lets Encrypt, have you made absolutely sure your site is accessible from outside of your network? + - Yes / No +- Have you searched for similar issues (both open and closed)? + - Yes / No **Describe the bug** -- A clear and concise description of what the bug is. -- What version of Nginx Proxy Manager is reported on the login page? + + + +**Nginx Proxy Manager Version** + + **To Reproduce** Steps to reproduce the behavior: @@ -23,14 +38,18 @@ Steps to reproduce the behavior: 3. Scroll down to '....' 4. See error + **Expected behavior** -A clear and concise description of what you expected to happen. + + **Screenshots** -If applicable, add screenshots to help explain your problem. + + **Operating System** -- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. + + **Additional context** -Add any other context about the problem here, docker version, browser version if applicable to the problem. Too much info is better than too little. + diff --git a/.github/ISSUE_TEMPLATE/dns_challenge_request.md b/.github/ISSUE_TEMPLATE/dns_challenge_request.md new file mode 100644 index 000000000..0a00f00f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/dns_challenge_request.md @@ -0,0 +1,18 @@ +--- +name: DNS challenge provider request +about: Suggest a new provider to be available for a certificate DNS challenge +title: '' +labels: dns provider request +assignees: '' + +--- + +**What provider would you like to see added to NPM?** + + + +**Have you checked if a certbot plugin exists?** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491ef..cf5b0f772 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,14 +7,26 @@ assignees: '' --- + + **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + **Describe the solution you'd like** -A clear and concise description of what you want to happen. + + **Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. + + **Additional context** -Add any other context or screenshots about the feature request here. + diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..f859b1278 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-label: 'stale' + stale-pr-label: 'stale' + stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:' + stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:' + close-issue-message: 'Issue was closed due to inactivity.' + close-pr-message: 'PR was closed due to inactivity.' + days-before-stale: 182 + days-before-close: 365 + operations-per-run: 50 diff --git a/.gitignore b/.gitignore index 184fbc3ca..fbb8167e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,9 @@ .DS_Store .idea ._* -node_modules -core* -config/development.json -dist -webpack_stats.html -data/* -yarn-error.log -yarn.lock -tmp -certbot.log - +.vscode +certbot-help.txt +test/node_modules +*/node_modules +docker/dev/dnsrouter-config.json.tmp +docker/dev/resolv.conf diff --git a/.version b/.version new file mode 100644 index 000000000..e4643748f --- /dev/null +++ b/.version @@ -0,0 +1 @@ +2.12.6 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a3f45064e..000000000 --- a/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM jc21/nginx-proxy-manager-base:latest - -MAINTAINER Jamie Curnow -LABEL maintainer="Jamie Curnow " - -ENV SUPPRESS_NO_CONFIG_WARNING=1 -ENV S6_FIX_ATTRS_HIDDEN=1 -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf - -# Nginx, Node and required packages should already be installed from the base image - -# root filesystem -COPY rootfs / - -# s6 overlay -RUN curl -L -o /tmp/s6-overlay-amd64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.4.0/s6-overlay-amd64.tar.gz" \ - && tar xzf /tmp/s6-overlay-amd64.tar.gz -C / - -# App -ENV NODE_ENV=production - -ADD dist /app/dist -ADD node_modules /app/node_modules -ADD src/backend /app/src/backend -ADD package.json /app/package.json -ADD knexfile.js /app/knexfile.js - -# Volumes -VOLUME [ "/data", "/etc/letsencrypt" ] -CMD [ "/init" ] - -# Ports -EXPOSE 80 -EXPOSE 81 -EXPOSE 443 -EXPOSE 9876 - -HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1 - diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 deleted file mode 100644 index 897a5fa82..000000000 --- a/Dockerfile.arm64 +++ /dev/null @@ -1,38 +0,0 @@ -FROM jc21/nginx-proxy-manager-base:arm64 - -MAINTAINER Jamie Curnow -LABEL maintainer="Jamie Curnow " - -ENV SUPPRESS_NO_CONFIG_WARNING=1 -ENV S6_FIX_ATTRS_HIDDEN=1 -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf - -# Nginx, Node and required packages should already be installed from the base image - -# root filesystem -COPY rootfs / - -# s6 overlay -RUN curl -L -o /tmp/s6-overlay-aarch64.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-aarch64.tar.gz" \ - && tar xzf /tmp/s6-overlay-aarch64.tar.gz -C / - -# App -ENV NODE_ENV=production - -ADD dist /app/dist -ADD node_modules /app/node_modules -ADD src/backend /app/src/backend -ADD package.json /app/package.json -ADD knexfile.js /app/knexfile.js - -# Volumes -VOLUME [ "/data", "/etc/letsencrypt" ] -CMD [ "/init" ] - -# Ports -EXPOSE 80 -EXPOSE 81 -EXPOSE 443 -EXPOSE 9876 - -HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1 diff --git a/Dockerfile.armv6l b/Dockerfile.armv6l deleted file mode 100644 index c08d09b4a..000000000 --- a/Dockerfile.armv6l +++ /dev/null @@ -1,38 +0,0 @@ -FROM jc21/nginx-proxy-manager-base:armv6 - -MAINTAINER Jamie Curnow -LABEL maintainer="Jamie Curnow " - -ENV SUPPRESS_NO_CONFIG_WARNING=1 -ENV S6_FIX_ATTRS_HIDDEN=1 -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf - -# Nginx, Node and required packages should already be installed from the base image - -# root filesystem -COPY rootfs / - -# s6 overlay -RUN curl -L -o /tmp/s6-overlay-arm.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-arm.tar.gz" \ - && tar xzf /tmp/s6-overlay-arm.tar.gz -C / - -# App -ENV NODE_ENV=production - -ADD dist /app/dist -ADD node_modules /app/node_modules -ADD src/backend /app/src/backend -ADD package.json /app/package.json -ADD knexfile.js /app/knexfile.js - -# Volumes -VOLUME [ "/data", "/etc/letsencrypt" ] -CMD [ "/init" ] - -# Ports -EXPOSE 80 -EXPOSE 81 -EXPOSE 443 -EXPOSE 9876 - -HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1 diff --git a/Dockerfile.armv7l b/Dockerfile.armv7l deleted file mode 100644 index af6b82a1b..000000000 --- a/Dockerfile.armv7l +++ /dev/null @@ -1,38 +0,0 @@ -FROM jc21/nginx-proxy-manager-base:armhf - -MAINTAINER Jamie Curnow -LABEL maintainer="Jamie Curnow " - -ENV SUPPRESS_NO_CONFIG_WARNING=1 -ENV S6_FIX_ATTRS_HIDDEN=1 -RUN echo "fs.file-max = 65535" > /etc/sysctl.conf - -# Nginx, Node and required packages should already be installed from the base image - -# root filesystem -COPY rootfs / - -# s6 overlay -RUN curl -L -o /tmp/s6-overlay-armhf.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.4.0/s6-overlay-armhf.tar.gz" \ - && tar xzf /tmp/s6-overlay-armhf.tar.gz -C / - -# App -ENV NODE_ENV=production - -ADD dist /app/dist -ADD node_modules /app/node_modules -ADD src/backend /app/src/backend -ADD package.json /app/package.json -ADD knexfile.js /app/knexfile.js - -# Volumes -VOLUME [ "/data", "/etc/letsencrypt" ] -CMD [ "/init" ] - -# Ports -EXPOSE 80 -EXPOSE 81 -EXPOSE 443 -EXPOSE 9876 - -HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1 diff --git a/Jenkinsfile b/Jenkinsfile index 530be9a1d..af913c2e0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,364 +1,285 @@ -pipeline { - options { - buildDiscarder(logRotator(numToKeepStr: '10')) - disableConcurrentBuilds() - } - agent any - environment { - IMAGE = "nginx-proxy-manager" - BASE_IMAGE = "jc21/${IMAGE}-base" - TEMP_IMAGE = "${IMAGE}-build_${BUILD_NUMBER}" - TAG_VERSION = getPackageVersion() - MAJOR_VERSION = "2" - BRANCH_LOWER = "${BRANCH_NAME.toLowerCase()}" - // Architectures: - AMD64_TAG = "amd64" - ARMV6_TAG = "armv6l" - ARMV7_TAG = "armv7l" - ARM64_TAG = "arm64" - } - stages { - stage('Build PR') { - when { - changeRequest() - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG}' - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' - - script { - def comment = pullRequest.comment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG}`") - } - } - } - } - stage('Build Develop') { - when { - branch 'develop' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:develop-${AMD64_TAG}' - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:develop-${AMD64_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' - } - } - } - stage('Build Master') { - when { - branch 'master' - } - parallel { - // ======================== - // amd64 - // ======================== - stage('amd64') { - agent { - label 'amd64' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${AMD64_TAG} .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${AMD64_TAG} docker.io/jc21/${IMAGE}:latest-${AMD64_TAG}' - - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:latest-${AMD64_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${AMD64_TAG}' - } - } - } - // ======================== - // arm64 - // ======================== - stage('arm64') { - agent { - label 'arm64' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARM64_TAG} -f Dockerfile.${ARM64_TAG} .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARM64_TAG} docker.io/jc21/${IMAGE}:latest-${ARM64_TAG}' - - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARM64_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${ARM64_TAG}' - } - } - } - // ======================== - // armv7l - // ======================== - stage('armv7l') { - agent { - label 'armv7l' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARMV7_TAG} -f Dockerfile.${ARMV7_TAG} .' +import groovy.transform.Field - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARMV7_TAG} docker.io/jc21/${IMAGE}:latest-${ARMV7_TAG}' +@Field +def shOutput = "" +def buildxPushTags = "" - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARMV7_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${ARMV7_TAG}' - } - } - } - // ======================== - // armv6l - Disabled for the time being - // ======================== - /* - stage('armv6l') { - agent { - label 'armv6l' - } - steps { - ansiColor('xterm') { - // Codebase - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} rm -rf node_modules' - sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod' - - // Docker Build - sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE}-${ARMV6_TAG} -f Dockerfile.${ARMV6_TAG} .' - - // Dockerhub - sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' - sh 'docker tag ${TEMP_IMAGE}-${ARMV6_TAG} docker.io/jc21/${IMAGE}:latest-${ARMV6_TAG}' - - withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { - sh "docker login -u '${duser}' -p '${dpass}'" - sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' - sh 'docker push docker.io/jc21/${IMAGE}:latest-${ARMV6_TAG}' - } - - sh 'docker rmi ${TEMP_IMAGE}-${ARMV6_TAG}' - } - } - } - */ - } - } - // ======================== - // latest manifest - // ======================== - stage('Latest Manifest') { - when { - branch 'master' - } - steps { - ansiColor('xterm') { - // ======================= - // latest - // ======================= - sh 'docker pull jc21/${IMAGE}:latest-${AMD64_TAG}' - sh 'docker pull jc21/${IMAGE}:latest-${ARM64_TAG}' - sh 'docker pull jc21/${IMAGE}:latest-${ARMV7_TAG}' - //sh 'docker pull jc21/${IMAGE}:latest-${ARMV6_TAG}' - - sh 'docker manifest push --purge jc21/${IMAGE}:latest || echo ""' - sh 'docker manifest create jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} jc21/${IMAGE}:latest-${ARM64_TAG} jc21/${IMAGE}:latest-${ARMV7_TAG}' - - sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} --arch ${AMD64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' - sh 'docker manifest push --purge jc21/${IMAGE}:latest' - - // ======================= - // major version - // ======================= - sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG}' - sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG}' - sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' - //sh 'docker pull jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG}' - - sh 'docker manifest push --purge jc21/${IMAGE}:${MAJOR_VERSION} || echo ""' - sh 'docker manifest create jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG}' - - sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} --arch ${AMD64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:${MAJOR_VERSION} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' - - // ======================= - // version - // ======================= - sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG}' - sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG}' - sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' - //sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' - - sh 'docker manifest push --purge jc21/${IMAGE}:${TAG_VERSION} || echo ""' - sh 'docker manifest create jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG}' - - sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} --arch ${AMD64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' - sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:${TAG_VERSION} jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' - } - } - } - // ======================== - // develop - // ======================== - stage('Develop Manifest') { - when { - branch 'develop' - } - steps { - ansiColor('xterm') { - sh 'docker pull jc21/${IMAGE}:develop-${AMD64_TAG}' - //sh 'docker pull jc21/${IMAGE}:develop-${ARM64_TAG}' - //sh 'docker pull jc21/${IMAGE}:develop-${ARMV7_TAG}' - //sh 'docker pull jc21/${IMAGE}:${TAG_VERSION}-${ARMV6_TAG}' - - sh 'docker manifest push --purge jc21/${IMAGE}:develop || :' - sh 'docker manifest create jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG}' +pipeline { + agent { + label 'docker-multiarch' + } + options { + buildDiscarder(logRotator(numToKeepStr: '5')) + disableConcurrentBuilds() + ansiColor('xterm') + } + environment { + IMAGE = 'nginx-proxy-manager' + BUILD_VERSION = getVersion() + MAJOR_VERSION = '2' + BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}" + BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" + COMPOSE_INTERACTIVE_NO_CLI = 1 + } + stages { + stage('Environment') { + parallel { + stage('Master') { + when { + branch 'master' + } + steps { + script { + buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" + } + } + } + stage('Other') { + when { + not { + branch 'master' + } + } + steps { + script { + // Defaults to the Branch name, which is applies to all branches AND pr's + buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}" + } + } + } + stage('Versions') { + 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' + } + } + stage('Docker Login') { + steps { + withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { + sh 'docker login -u "${duser}" -p "${dpass}"' + } + } + } + } + } + stage('Builds') { + parallel { + stage('Project') { + steps { + script { + // Frontend and Backend + def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: ''' + set -e + ./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1 + ./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1 + ''') + shOutput = readFile "${env.WORKSPACE}/tmp-sh-build" + if (shStatusCode != 0) { + error "${shOutput}" + } + } + } + post { + always { + sh 'rm -f ${WORKSPACE}/tmp-sh-build' + } + failure { + npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true) + } + } + } + stage('Docs') { + steps { + dir(path: 'docs') { + sh 'yarn install' + sh 'yarn build' + } + } + } + } + } + stage('Test Sqlite') { + environment { + COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite" + COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml' + } + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + steps { + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + } + post { + always { + // Dumps to analyze later + 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' + } + unstable { + dir(path: 'test/results') { + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') + } + } + } + } + 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 { + sh 'rm -rf ./test/results/junit/*' + sh './scripts/ci/fulltest-cypress' + } + post { + always { + // Dumps to analyze later + 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' + } + unstable { + dir(path: 'test/results') { + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') + } + } + } + } + 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 { + 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-postgres.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' + + junit 'test/results/junit/*' + sh 'docker-compose down --remove-orphans --volumes -t 30 || true' + } + unstable { + dir(path: 'test/results') { + archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') + } + } + } + } + stage('MultiArch Build') { + when { + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + steps { + sh "./scripts/buildx --push ${buildxPushTags}" + } + } + stage('Docs / Comment') { + parallel { + stage('Docs Job') { + when { + allOf { + branch pattern: "^(develop|master)\$", comparator: "REGEXP" + not { + equals expected: 'UNSTABLE', actual: currentBuild.result + } + } + } + steps { + build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")] + } + } + 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/nginxproxymanager/${IMAGE}-dev): +``` +nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER} +``` + +> [!NOTE] +> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. +> This is a different docker image namespace than the official image. + +> [!WARNING] +> Changes and additions to DNS Providers require verification by at least 2 members of the community! +""", true) + } + } + } + } + } + } + post { + always { + sh 'echo Reverting ownership' + sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data' + printResult(true) + } + failure { + archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) + } + unstable { + archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) + } + } +} - sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG} --arch ${AMD64_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARM64_TAG} --os linux --arch ${ARM64_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARMV7_TAG} --os linux --arch arm --variant ${ARMV7_TAG}' - //sh 'docker manifest annotate jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${ARMV6_TAG} --os linux --arch arm --variant ${ARMV6_TAG}' - } - } - } - // ======================== - // cleanup - // ======================== - stage('Latest Cleanup') { - when { - branch 'master' - } - steps { - ansiColor('xterm') { - sh 'docker rmi jc21/${IMAGE}:latest jc21/${IMAGE}:latest-${AMD64_TAG} jc21/${IMAGE}:latest-${ARM64_TAG} jc21/${IMAGE}:latest-${ARMV7_TAG} || echo ""' - sh 'docker rmi jc21/${IMAGE}:${MAJOR_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${MAJOR_VERSION}-${ARMV7_TAG} || echo ""' - sh 'docker rmi jc21/${IMAGE}:${TAG_VERSION}-${AMD64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARM64_TAG} jc21/${IMAGE}:${TAG_VERSION}-${ARMV7_TAG} || echo ""' - } - } - } - stage('Develop Cleanup') { - when { - branch 'develop' - } - steps { - ansiColor('xterm') { - sh 'docker rmi jc21/${IMAGE}:develop jc21/${IMAGE}:develop-${AMD64_TAG} || echo ""' - } - } - } - stage('PR Cleanup') { - when { - changeRequest() - } - steps { - ansiColor('xterm') { - sh 'docker rmi jc21/${IMAGE}:github-${BRANCH_LOWER}-${AMD64_TAG} || echo ""' - } - } - } - } - post { - success { - juxtapose event: 'success' - sh 'figlet "SUCCESS"' - } - failure { - juxtapose event: 'failure' - sh 'figlet "FAILURE"' - } - always { - sh 'echo Reverting ownership' - sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} chown -R $(id -u):$(id -g) /data' - } - } +def getVersion() { + ver = sh(script: 'cat .version', returnStdout: true) + return ver.trim() } -def getPackageVersion() { - ver = sh(script: 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} bash -c "cat /data/package.json|jq -r \'.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 a0270ba30..2116a55ae 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ -![Nginx Proxy Manager](https://public.jc21.com/nginx-proxy-manager/github.png "Nginx Proxy Manager") - -# Nginx Proxy Manager - -![Version](https://img.shields.io/badge/version-2.0.14-green.svg?style=for-the-badge) -![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) -![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) +

+ +

+ + + + + + + +

This project comes as a pre-built docker image that enables you to easily forward to your websites running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. +- [Quick Setup](#quick-setup) +- [Full Setup](https://nginxproxymanager.com/setup/) +- [Screenshots](https://nginxproxymanager.com/screenshots/) ## Project Goal -I created this project to fill a personal need to provide users with a easy way to accomplish reverse +I created this project to fill a personal need to provide users with an easy way to accomplish reverse proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed. While there might be advanced options they are optional and the project should be as simple as possible so that the barrier for entry here is low. @@ -30,46 +37,58 @@ so that the barrier for entry here is low. - User management, permissions and audit log -## Screenshots - -[![Login](https://public.jc21.com/nginx-proxy-manager/v2/small/login.jpg "Login")](https://public.jc21.com/nginx-proxy-manager/v2/large/login.jpg) -[![Dashboard](https://public.jc21.com/nginx-proxy-manager/v2/small/dashboard.jpg "Dashboard")](https://public.jc21.com/nginx-proxy-manager/v2/large/dashboard.jpg) -[![Proxy Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts.jpg "Proxy Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts.jpg) -[![Proxy Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts-new1.jpg "Proxy Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts-new1.jpg) -[![Proxy Host SSL](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts-new2.jpg "Proxy Host SSL")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts-new2.jpg) -[![Redirection Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/redirection-hosts.jpg "Redirection Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/redirection-hosts.jpg) -[![Redirection Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/redirection-hosts-new1.jpg "Redirection Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/redirection-hosts-new1.jpg) -[![Streams](https://public.jc21.com/nginx-proxy-manager/v2/small/streams.jpg "Streams")](https://public.jc21.com/nginx-proxy-manager/v2/large/streams.jpg) -[![Stream Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/streams-new1.jpg "Stream Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/streams-new1.jpg) -[![404 Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/dead-hosts.jpg "404 Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/dead-hosts.jpg) -[![404 Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/dead-hosts-new1.jpg "404 Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/dead-hosts-new1.jpg) -[![Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates.jpg "Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates.jpg) -[![Lets Encrypt Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates-new1.jpg "Lets Encrypt Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates-new1.jpg) -[![Custom Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates-new2.jpg "Custom Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates-new2.jpg) -[![Access Lists](https://public.jc21.com/nginx-proxy-manager/v2/small/access-lists.jpg "Access Lists")](https://public.jc21.com/nginx-proxy-manager/v2/large/access-lists.jpg) -[![Access List Users](https://public.jc21.com/nginx-proxy-manager/v2/small/access-lists-new1.jpg "Access List Users")](https://public.jc21.com/nginx-proxy-manager/v2/large/access-lists-new1.jpg) -[![Users](https://public.jc21.com/nginx-proxy-manager/v2/small/users.jpg "Users")](https://public.jc21.com/nginx-proxy-manager/v2/large/users.jpg) -[![User Permissions](https://public.jc21.com/nginx-proxy-manager/v2/small/users-permissions.jpg "User Permissions")](https://public.jc21.com/nginx-proxy-manager/v2/large/users-permissions.jpg) -[![Audit Log](https://public.jc21.com/nginx-proxy-manager/v2/small/audit-log.jpg "Audit Log")](https://public.jc21.com/nginx-proxy-manager/v2/large/audit-log.jpg) +## Hosting your home network +I won't go in to too much detail here but here are the basics for someone new to this self-hosted world. -## Getting started +1. Your home router will have a Port Forwarding section somewhere. Log in and find it +2. Add port forwarding for port 80 and 443 to the server hosting this project +3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) +4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services + +## Quick Setup + +1. Install Docker and Docker-Compose + +- [Docker Install documentation](https://docs.docker.com/install/) +- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) + +2. Create a docker-compose.yml file similar to this: + +```yml +services: + app: + image: 'docker.io/jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt +``` -Please consult the [installation instructions](doc/INSTALL.md) for a complete guide or -if you just want to get up and running in the quickest time possible, grab all the files in the `doc/example/` folder and run `docker-compose up -d` +This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. +3. Bring up your stack by running -## Administration +```bash +docker-compose up -d -When your docker container is running, connect to it on port `81` for the admin interface. +# If using docker-compose-plugin +docker compose up -d -[http://localhost:81](http://localhost:81) +``` -Note: Requesting SSL Certificates won't work until this project is accessible from the outside world, as explained below. +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. -### Default Administrator User +[http://127.0.0.1:81](http://127.0.0.1:81) +Default Admin User: ``` Email: admin@example.com Password: changeme @@ -78,22 +97,24 @@ Password: changeme Immediately after logging in with this default user you will be asked to modify your details and change your password. -## Hosting your home network +## Contributing -I won't go in to too much detail here but here are the basics for someone new to this self-hosted world. +All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. + +CI is used in this project. All PR's must pass before being considered. After passing, +docker builds for PR's are available on dockerhub for manual verifications. + +Documentation within the `develop` branch is available for preview at +[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com) -1. Your home router will have a Port Forwarding section somewhere. Log in and find it -2. Add port forwarding for port 80 and 443 to the server hosting this project -3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS -4. Use the Nginx Proxy Manager here as your gateway to forward to your other web based services +### Contributors -## Nginx Proxy Manager in the wild +Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). -As this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documenation of those -integrations, they are *not supported* by me and any donation links on the pages of those integrations will not come to me even though it looks like it. -Known integrations: +## Getting Support -- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) -- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) +1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) +2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) +3. [Reddit](https://reddit.com/r/nginxproxymanager) diff --git a/backend/.eslintrc.json b/backend/.eslintrc.json new file mode 100644 index 000000000..6d6172a48 --- /dev/null +++ b/backend/.eslintrc.json @@ -0,0 +1,73 @@ +{ + "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 new file mode 100644 index 000000000..149080b91 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,8 @@ +config/development.json +data/* +yarn-error.log +tmp +certbot.log +node_modules +core.* + diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 000000000..fefbcfa6d --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 320, + "tabWidth": 4, + "useTabs": true, + "semi": true, + "singleQuote": true, + "bracketSpacing": true, + "jsxBracketSameLine": true, + "trailingComma": "all", + "proseWrap": "always" +} diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 000000000..59f7def20 --- /dev/null +++ b/backend/app.js @@ -0,0 +1,90 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const fileUpload = require('express-fileupload'); +const compression = require('compression'); +const config = require('./lib/config'); +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 (config.debug()) { + 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/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 (config.debug() || (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 (config.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/config/README.md b/backend/config/README.md similarity index 100% rename from config/README.md rename to backend/config/README.md diff --git a/config/default.json b/backend/config/default.json similarity index 83% rename from config/default.json rename to backend/config/default.json index 64ab577c8..154e66e48 100644 --- a/config/default.json +++ b/backend/config/default.json @@ -1,6 +1,6 @@ { "database": { - "engine": "mysql", + "engine": "mysql2", "host": "db", "name": "npm", "user": "npm", diff --git a/backend/config/sqlite-test-db.json b/backend/config/sqlite-test-db.json new file mode 100644 index 000000000..ad5488651 --- /dev/null +++ b/backend/config/sqlite-test-db.json @@ -0,0 +1,26 @@ +{ + "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 new file mode 100644 index 000000000..1a8b1634e --- /dev/null +++ b/backend/db.js @@ -0,0 +1,27 @@ +const config = require('./lib/config'); + +if (!config.has('database')) { + throw new Error('Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/'); +} + +function generateDbConfig() { + const cfg = config.get('database'); + if (cfg.engine === 'knex-native') { + return cfg.knex; + } + return { + client: cfg.engine, + connection: { + host: cfg.host, + user: cfg.user, + password: cfg.password, + database: cfg.name, + port: cfg.port + }, + migrations: { + tableName: 'migrations' + } + }; +} + +module.exports = require('knex')(generateDbConfig()); diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 000000000..d334a7c2d --- /dev/null +++ b/backend/index.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +const schema = require('./schema'); +const logger = require('./logger').global; + +const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== 'false'; + +async function appStart () { + const migrate = require('./migrate'); + const setup = require('./setup'); + const app = require('./app'); + const internalCertificate = require('./internal/certificate'); + const internalIpRanges = require('./internal/ip_ranges'); + + return migrate.latest() + .then(setup) + .then(schema.getCompiledSchema) + .then(() => { + if (IP_RANGES_FETCH_ENABLED) { + logger.info('IP Ranges fetch is enabled'); + return internalIpRanges.fetch().catch((err) => { + logger.error('IP Ranges fetch failed, continuing anyway:', err.message); + }); + } else { + logger.info('IP Ranges fetch is disabled by environment variable'); + } + }) + .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, err); + setTimeout(appStart, 1000); + }); +} + +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 new file mode 100644 index 000000000..f6043e18b --- /dev/null +++ b/backend/internal/access-list.js @@ -0,0 +1,540 @@ +const _ = require('lodash'); +const fs = require('fs'); +const batchflow = require('batchflow'); +const logger = require('../logger').access; +const error = require('../lib/error'); +const utils = require('../lib/utils'); +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'); + +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() + .insertAndFetch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + owner_user_id: access.token.getUserId(1) + }) + .then(utils.omitRow(omissions())); + }) + .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 (parseInt(row.proxy_host_count, 10)) { + return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); + } + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'created', + object_type: 'access-list', + object_id: row.id, + meta: internalAccessList.maskItems(data) + }); + }) + .then(() => { + return internalAccessList.maskItems(row); + }); + }); + }, + + /** + * @param {Access} access + * @param {Object} data + * @param {Integer} data.id + * @param {String} [data.name] + * @param {String} [data.items] + * @return {Promise} + */ + update: (access, data) => { + return access.can('access_lists:update', data.id) + .then((/*access_data*/) => { + return internalAccessList.get(access, {id: data.id}); + }) + .then((row) => { + if (row.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id); + } + }) + .then(() => { + // patch name if specified + if (typeof data.name !== 'undefined' && data.name) { + return accessListModel + .query() + .where({id: data.id}) + .patch({ + name: data.name, + satisfy_any: data.satisfy_any, + pass_auth: data.pass_auth, + }); + } + }) + .then(() => { + // Check for items and add/update/remove them + if (typeof data.items !== 'undefined' && data.items) { + let promises = []; + let items_to_keep = []; + + data.items.map(function (item) { + if (item.password) { + promises.push(accessListAuthModel + .query() + .insert({ + access_list_id: data.id, + username: item.username, + password: item.password + }) + ); + } else { + // This was supplied with an empty password, which means keep it but don't change the password + items_to_keep.push(item.username); + } + }); + + let query = accessListAuthModel + .query() + .delete() + .where('access_list_id', data.id); + + if (items_to_keep.length) { + query.andWhere('username', 'NOT IN', items_to_keep); + } + + return query + .then(() => { + // Add new items + if (promises.length) { + return Promise.all(promises); + } + }); + } + }) + .then(() => { + // Check for clients and add/update/remove them + if (typeof data.clients !== 'undefined' && data.clients) { + let promises = []; + + data.clients.map(function (client) { + if (client.address) { + promises.push(accessListClientModel + .query() + .insert({ + access_list_id: data.id, + address: client.address, + directive: client.directive + }) + ); + } + }); + + let query = accessListClientModel + .query() + .delete() + .where('access_list_id', data.id); + + return query + .then(() => { + // Add new items + if (promises.length) { + return Promise.all(promises); + } + }); + } + }) + .then(() => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'access-list', + object_id: data.id, + meta: internalAccessList.maskItems(data) + }); + }) + .then(() => { + // re-fetch with expansions + return internalAccessList.get(access, { + id: data.id, + expand: ['owner', 'items', 'clients', 'proxy_hosts.[certificate,access_list.[clients,items]]'] + }, true /* <- skip masking */); + }) + .then((row) => { + return internalAccessList.build(row) + .then(() => { + if (parseInt(row.proxy_host_count, 10)) { + return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts); + } + }).then(internalNginx.reload) + .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')) + .leftJoin('proxy_host', function() { + this.on('proxy_host.access_list_id', '=', 'access_list.id') + .andOn('proxy_host.is_deleted', '=', 0); + }) + .where('access_list.is_deleted', 0) + .andWhere('access_list.id', data.id) + .groupBy('access_list.id') + .allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('access_list.owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + if (!skip_masking && typeof row.items !== 'undefined' && row.items) { + row = internalAccessList.maskItems(row); + } + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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')) + .leftJoin('proxy_host', function() { + this.on('proxy_host.access_list_id', '=', 'access_list.id') + .andOn('proxy_host.is_deleted', '=', 0); + }) + .where('access_list.is_deleted', 0) + .groupBy('access_list.id') + .allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }) + .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.execFile('openssl', ['passwd', '-apr1', item.password]) + .then((res) => { + try { + fs.appendFileSync(htpasswd_file, item.username + ':' + res + '\n', {encoding: 'utf8'}); + } catch (err) { + reject(err); + } + 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/audit-log.js b/backend/internal/audit-log.js new file mode 100644 index 000000000..60bdd2efa --- /dev/null +++ b/backend/internal/audit-log.js @@ -0,0 +1,79 @@ +const error = require('../lib/error'); +const auditLogModel = require('../models/audit-log'); +const {castJsonIfNeed} = require('../lib/helpers'); + +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) + .allowGraph('[user]'); + + // Query is used for searching + if (typeof search_query === 'string' && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed('meta'), 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched('[' + 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 new file mode 100644 index 000000000..f2e845a24 --- /dev/null +++ b/backend/internal/certificate.js @@ -0,0 +1,1244 @@ +const _ = require('lodash'); +const fs = require('fs'); +const https = require('https'); +const tempWrite = require('temp-write'); +const moment = require('moment'); +const archiver = require('archiver'); +const path = require('path'); +const { isArray } = require('lodash'); +const logger = require('../logger').ssl; +const config = require('../lib/config'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const certbot = require('../lib/certbot'); +const certificateModel = require('../models/certificate'); +const tokenModel = require('../models/token'); +const dnsPlugins = require('../global/certbot-dns-plugins.json'); +const internalAuditLog = require('./audit-log'); +const internalNginx = require('./nginx'); +const internalHost = require('./host'); + + +const letsencryptStaging = config.useLetsencryptStaging(); +const letsencryptServer = config.useLetsencryptServer(); +const letsencryptConfig = '/etc/letsencrypt.ini'; +const certbotCommand = 'certbot'; + +function omissions() { + return ['is_deleted', 'owner.is_deleted']; +} + +const internalCertificate = { + + allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'], + intervalTimeout: 1000 * 60 * 60, // 1 hour + interval: null, + intervalProcessing: false, + renewBeforeExpirationBy: [30, 'days'], + + 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 expiring within ' + internalCertificate.renewBeforeExpirationBy[0] + ' ' + internalCertificate.renewBeforeExpirationBy[1] + ' ...'); + + const expirationThreshold = moment().add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]).format('YYYY-MM-DD HH:mm:ss'); + + // Fetch all the letsencrypt certs from the db that will expire within the configured threshold + certificateModel + .query() + .where('is_deleted', 0) + .andWhere('provider', 'letsencrypt') + .andWhere('expires_on', '<', expirationThreshold) + .then((certificates) => { + if (!certificates || !certificates.length) { + return null; + } + + /** + * Renews must be run sequentially or we'll get an error 'Another + * instance of Certbot is already running.' + */ + let sequence = Promise.resolve(); + + certificates.forEach(function (certificate) { + sequence = sequence.then(() => + internalCertificate + .renew( + { + can: () => + Promise.resolve({ + permission_visibility: 'all', + }), + token: new tokenModel(), + }, + { id: certificate.id }, + ) + .catch((err) => { + // Don't want to stop the train here, just log the error + logger.error(err.message); + }), + ); + }); + + return sequence; + }) + .then(() => { + logger.info('Completed SSL cert renew process'); + 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() + .insertAndFetch(data) + .then(utils.omitRow(omissions())); + }) + .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(utils.omitRow(omissions())) + .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() + .patchAndFetchById(row.id, data) + .then(utils.omitRow(omissions())) + .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 saved_row; + }); + }); + }); + }, + + /** + * @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) + .allowGraph('[owner]') + .allowGraph('[proxy_hosts]') + .allowGraph('[redirection_hosts]') + .allowGraph('[dead_hosts]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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') + .allowGraph('[owner]') + .allowGraph('[proxy_hosts]') + .allowGraph('[redirection_hosts]') + .allowGraph('[dead_hosts]') + .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.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }); + }, + + /** + * 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) => { + 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) => { + // Examples: + // subject=CN = *.jc21.com + // subject=CN = something.example.com + const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; + const match = regex.exec(result); + if (match && typeof match[1] !== 'undefined') { + certData['cn'] = match[1]; + } + }) + .then(() => { + return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout'); + }) + + .then((result) => { + // Examples: + // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 + // issuer=C = US, O = Let's Encrypt, CN = E5 + // issuer=O = NginxProxyManager, CN = NginxProxyManager Intermediate CA","O = NginxProxyManager, CN = NginxProxyManager Intermediate CA + const regex = /^(?:issuer=)?(.*)$/gim; + const match = regex.exec(result); + if (match && typeof match[1] !== 'undefined') { + 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}' ` + + '--work-dir "/tmp/letsencrypt-lib" ' + + '--logs-dir "/tmp/letsencrypt-log" ' + + `--cert-name "npm-${certificate.id}" ` + + '--agree-tos ' + + '--authenticator webroot ' + + `--email '${certificate.meta.letsencrypt_email}' ` + + '--preferred-challenges "dns,http" ' + + `--domains "${certificate.domain_names.join(',')}" ` + + (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + + (letsencryptStaging && letsencryptServer === null ? '--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.json`) + * @param {String | null} credentials the content of this providers credentials file + * @param {String} propagation_seconds + * @returns {Promise} + */ + requestLetsEncryptSslWithDnsChallenge: async (certificate) => { + await certbot.installPlugin(certificate.meta.dns_provider); + const dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + logger.info(`Requesting Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); + + const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id; + fs.mkdirSync('/etc/letsencrypt/credentials', { recursive: true }); + fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, {mode: 0o600}); + + // Whether the plugin has a ---credentials argument + const hasConfigArg = certificate.meta.dns_provider !== 'route53'; + + let mainCmd = certbotCommand + ' certonly ' + + `--config '${letsencryptConfig}' ` + + '--work-dir "/tmp/letsencrypt-lib" ' + + '--logs-dir "/tmp/letsencrypt-log" ' + + `--cert-name 'npm-${certificate.id}' ` + + '--agree-tos ' + + `--email '${certificate.meta.letsencrypt_email}' ` + + `--domains '${certificate.domain_names.join(',')}' ` + + `--authenticator '${dnsPlugin.full_plugin_name}' ` + + ( + hasConfigArg + ? `--${dnsPlugin.full_plugin_name}-credentials '${credentialsLocation}' ` + : '' + ) + + ( + certificate.meta.propagation_seconds !== undefined + ? `--${dnsPlugin.full_plugin_name}-propagation-seconds '${certificate.meta.propagation_seconds}' ` + : '' + ) + + (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + + (letsencryptStaging && letsencryptServer === null ? '--staging ' : ''); + + // Prepend the path to the credentials file as an environment variable + if (certificate.meta.dns_provider === 'route53') { + mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd; + } + + if (certificate.meta.dns_provider === 'duckdns') { + mainCmd = mainCmd + ' --dns-duckdns-no-txt-restore'; + } + + logger.info('Command:', mainCmd); + + try { + const result = await utils.exec(mainCmd); + logger.info(result); + return result; + } catch (err) { + // Don't fail if file does not exist, so no need for action in the callback + fs.unlink(credentialsLocation, () => {}); + 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}' ` + + '--work-dir "/tmp/letsencrypt-lib" ' + + '--logs-dir "/tmp/letsencrypt-log" ' + + `--cert-name 'npm-${certificate.id}' ` + + '--preferred-challenges "dns,http" ' + + '--no-random-sleep-on-renew ' + + '--disable-hook-validation ' + + (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + + (letsencryptStaging && letsencryptServer === null ? '--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 dnsPlugin = dnsPlugins[certificate.meta.dns_provider]; + + if (!dnsPlugin) { + throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`); + } + + logger.info(`Renewing Let'sEncrypt certificates via ${dnsPlugin.name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`); + + let mainCmd = certbotCommand + ' renew --force-renewal ' + + `--config "${letsencryptConfig}" ` + + '--work-dir "/tmp/letsencrypt-lib" ' + + '--logs-dir "/tmp/letsencrypt-log" ' + + `--cert-name 'npm-${certificate.id}' ` + + '--disable-hook-validation ' + + '--no-random-sleep-on-renew ' + + (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + + (letsencryptStaging && letsencryptServer === null ? '--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}' ` + + '--work-dir "/tmp/letsencrypt-lib" ' + + '--logs-dir "/tmp/letsencrypt-log" ' + + `--cert-path '/etc/letsencrypt/live/npm-${certificate.id}/fullchain.pem' ` + + '--delete-after-revoke ' + + (letsencryptServer !== null ? `--server '${letsencryptServer}' ` : '') + + (letsencryptStaging && letsencryptServer === null ? '--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: { + 'User-Agent': 'Mozilla/5.0', + '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 () { + try { + const parsedBody = JSON.parse(responseBody + ''); + if (res.statusCode !== 200) { + logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`); + resolve(undefined); + } else { + resolve(parsedBody); + } + } catch (err) { + if (res.statusCode !== 200) { + logger.warn(`Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`); + } else { + logger.warn(`Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`); + } + resolve(undefined); + } + }); + }); + + // 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.error) { + logger.info(`HTTP challenge test failed for domain ${domain} because error was returned: ${result.error.msg}`); + return `other:${result.error.msg}`; + } 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/dead-host.js b/backend/internal/dead-host.js new file mode 100644 index 000000000..6bbdf61be --- /dev/null +++ b/backend/internal/dead-host.js @@ -0,0 +1,465 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const deadHostModel = require('../models/dead_host'); +const internalHost = require('./host'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const {castJsonIfNeed} = require('../lib/helpers'); + +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); + + // Fix for db field not having a default value + // for this optional field. + if (typeof data.advanced_config === 'undefined') { + data.advanced_config = ''; + } + + return deadHostModel + .query() + .insertAndFetch(data) + .then(utils.omitRow(omissions())); + }) + .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) + .allowGraph('[owner,certificate]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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 || !row.id) { + 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 || !row.id) { + 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') + .allowGraph('[owner,certificate]') + .orderBy(castJsonIfNeed('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' && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed('domain_names'), 'like', '%' + search_query + '%'); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }) + .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/host.js b/backend/internal/host.js new file mode 100644 index 000000000..52c6d2bda --- /dev/null +++ b/backend/internal/host.js @@ -0,0 +1,236 @@ +const _ = require('lodash'); +const proxyHostModel = require('../models/proxy_host'); +const redirectionHostModel = require('../models/redirection_host'); +const deadHostModel = require('../models/dead_host'); +const {castJsonIfNeed} = require('../lib/helpers'); + +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; + + const 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) { + const 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) { + const promises = [ + proxyHostModel + .query() + .where('is_deleted', 0) + .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'), + redirectionHostModel + .query() + .where('is_deleted', 0) + .andWhere(castJsonIfNeed('domain_names'), 'like', '%' + hostname + '%'), + deadHostModel + .query() + .where('is_deleted', 0) + .andWhere(castJsonIfNeed('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 new file mode 100644 index 000000000..d34ee5a1b --- /dev/null +++ b/backend/internal/ip_ranges.js @@ -0,0 +1,147 @@ +const https = require('https'); +const fs = require('fs'); +const logger = require('../logger').ip_ranges; +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const internalNginx = require('./nginx'); + +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) => { + const renderEngine = utils.getRenderEngine(); + 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/nginx.js b/backend/internal/nginx.js new file mode 100644 index 000000000..5f802c004 --- /dev/null +++ b/backend/internal/nginx.js @@ -0,0 +1,438 @@ +const _ = require('lodash'); +const fs = require('fs'); +const logger = require('../logger').nginx; +const config = require('../lib/config'); +const utils = require('../lib/utils'); +const error = require('../lib/error'); + +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. + // Don't throw errors, as the file may not exist at all + // Delete the .err file too + return internalNginx.deleteConfig(host_type, host, false, true); + }) + .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 (config.debug()) { + 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(() => { + internalNginx.renameConfigAsError(host_type, host); + }) + .then(() => { + return internalNginx.deleteConfig(host_type, host, true); + }); + }); + }) + .then(() => { + return internalNginx.reload(); + }) + .then(() => { + return combined_meta; + }); + }, + + /** + * @returns {Promise} + */ + test: () => { + if (config.debug()) { + 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) => { + if (host_type === 'default') { + return '/data/nginx/default_host/site.conf'; + } + return '/data/nginx/' + internalNginx.getFileFriendlyHostType(host_type) + '/' + host_id + '.conf'; + }, + + /** + * Generates custom locations + * @param {Object} host + * @returns {Promise} + */ + renderLocations: (host) => { + 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; + } + + const renderEngine = utils.getRenderEngine(); + 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('/')}`; + } + + // eslint-disable-next-line + renderedLocations += await renderEngine.parseAndRender(template, locationCopy); + } + + }; + + locationRendering().then(() => resolve(renderedLocations)); + + }); + }, + + /** + * @param {String} host_type + * @param {Object} host + * @returns {Promise} + */ + generateConfig: (host_type, host_row) => { + // Prevent modifying the original object: + let host = JSON.parse(JSON.stringify(host_row)); + const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); + + if (config.debug()) { + logger.info('Generating ' + nice_host_type + ' Config:', JSON.stringify(host, null, 2)); + } + + const renderEngine = utils.getRenderEngine(); + + return new Promise((resolve, reject) => { + let template = null; + let filename = internalNginx.getConfigName(nice_host_type, host.id); + + try { + template = fs.readFileSync(__dirname + '/../templates/' + nice_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 (nice_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 (config.debug()) { + logger.success('Wrote config:', filename, config_text); + } + + // Restore locations array + host.locations = origLocations; + + resolve(true); + }) + .catch((err) => { + if (config.debug()) { + 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 (config.debug()) { + logger.info('Generating LetsEncrypt Request Config:', certificate); + } + + const renderEngine = utils.getRenderEngine(); + + 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 (config.debug()) { + logger.success('Wrote config:', filename, config_text); + } + + resolve(true); + }) + .catch((err) => { + if (config.debug()) { + logger.warn('Could not write ' + filename + ':', err.message); + } + + reject(new error.ConfigurationError(err.message)); + }); + }); + }, + + /** + * A simple wrapper around unlinkSync that writes to the logger + * + * @param {String} filename + */ + deleteFile: (filename) => { + logger.debug('Deleting file: ' + filename); + try { + fs.unlinkSync(filename); + } catch (err) { + logger.debug('Could not delete file:', JSON.stringify(err, null, 2)); + } + }, + + /** + * + * @param {String} host_type + * @returns String + */ + getFileFriendlyHostType: (host_type) => { + return host_type.replace(new RegExp('-', 'g'), '_'); + }, + + /** + * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` + * + * @param {Object} certificate + * @returns {Promise} + */ + deleteLetsEncryptRequestConfig: (certificate) => { + const config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf'; + return new Promise((resolve/*, reject*/) => { + internalNginx.deleteFile(config_file); + resolve(); + }); + }, + + /** + * @param {String} host_type + * @param {Object} [host] + * @param {Boolean} [delete_err_file] + * @returns {Promise} + */ + deleteConfig: (host_type, host, delete_err_file) => { + const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); + const config_file_err = config_file + '.err'; + + return new Promise((resolve/*, reject*/) => { + internalNginx.deleteFile(config_file); + if (delete_err_file) { + internalNginx.deleteFile(config_file_err); + } + resolve(); + }); + }, + + /** + * @param {String} host_type + * @param {Object} [host] + * @returns {Promise} + */ + renameConfigAsError: (host_type, host) => { + const config_file = internalNginx.getConfigName(internalNginx.getFileFriendlyHostType(host_type), typeof host === 'undefined' ? 0 : host.id); + const config_file_err = config_file + '.err'; + + return new Promise((resolve/*, reject*/) => { + fs.unlink(config_file, () => { + // ignore result, continue + fs.rename(config_file, config_file_err, () => { + // also ignore result, as this is a debugging informative file anyway + 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 + * @returns {Promise} + */ + bulkDeleteConfigs: (host_type, hosts) => { + let promises = []; + hosts.map(function (host) { + promises.push(internalNginx.deleteConfig(host_type, host, true)); + }); + + return Promise.all(promises); + }, + + /** + * @param {string} config + * @returns {boolean} + */ + advancedConfigHasDefaultLocation: function (cfg) { + return !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im); + }, + + /** + * @returns {boolean} + */ + ipv6Enabled: function () { + if (typeof process.env.DISABLE_IPV6 !== 'undefined') { + const disabled = process.env.DISABLE_IPV6.toLowerCase(); + return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes'); + } + + return true; + } +}; + +module.exports = internalNginx; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js new file mode 100644 index 000000000..32f2bc0dc --- /dev/null +++ b/backend/internal/proxy-host.js @@ -0,0 +1,472 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const proxyHostModel = require('../models/proxy_host'); +const internalHost = require('./host'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const {castJsonIfNeed} = require('../lib/helpers'); + +function omissions () { + return ['is_deleted', 'owner.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); + + // Fix for db field not having a default value + // for this optional field. + if (typeof data.advanced_config === 'undefined') { + data.advanced_config = ''; + } + + return proxyHostModel + .query() + .insertAndFetch(data) + .then(utils.omitRow(omissions())); + }) + .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(utils.omitRow(omissions())) + .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 saved_row; + }); + }); + }) + .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) + .allowGraph('[owner,access_list.[clients,items],certificate]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + row = internalHost.cleanRowCertificateMeta(row); + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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 || !row.id) { + 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 || !row.id) { + 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') + .allowGraph('[owner,access_list,certificate]') + .orderBy(castJsonIfNeed('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' && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }) + .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 new file mode 100644 index 000000000..6a81b8662 --- /dev/null +++ b/backend/internal/redirection-host.js @@ -0,0 +1,465 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const redirectionHostModel = require('../models/redirection_host'); +const internalHost = require('./host'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const {castJsonIfNeed} = require('../lib/helpers'); + +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); + + // Fix for db field not having a default value + // for this optional field. + if (typeof data.advanced_config === 'undefined') { + data.advanced_config = ''; + } + + return redirectionHostModel + .query() + .insertAndFetch(data) + .then(utils.omitRow(omissions())); + }) + .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; + }); + } + 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) + .allowGraph('[owner,certificate]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + row = internalHost.cleanRowCertificateMeta(row); + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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 || !row.id) { + 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 || !row.id) { + 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') + .allowGraph('[owner,certificate]') + .orderBy(castJsonIfNeed('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' && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed('domain_names'), 'like', `%${search_query}%`); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }) + .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 new file mode 100644 index 000000000..4dde659bd --- /dev/null +++ b/backend/internal/report.js @@ -0,0 +1,38 @@ +const internalProxyHost = require('./proxy-host'); +const internalRedirectionHost = require('./redirection-host'); +const internalDeadHost = require('./dead-host'); +const internalStream = require('./stream'); + +const internalReport = { + + /** + * @param {Access} access + * @return {Promise} + */ + getHostsReport: (access) => { + return access.can('reports:hosts', 1) + .then((access_data) => { + let user_id = access.token.getUserId(1); + + let promises = [ + internalProxyHost.getCount(user_id, access_data.visibility), + internalRedirectionHost.getCount(user_id, access_data.visibility), + internalStream.getCount(user_id, access_data.visibility), + internalDeadHost.getCount(user_id, access_data.visibility) + ]; + + return Promise.all(promises); + }) + .then((counts) => { + return { + proxy: counts.shift(), + redirection: counts.shift(), + stream: counts.shift(), + dead: counts.shift() + }; + }); + + } +}; + +module.exports = internalReport; diff --git a/backend/internal/setting.js b/backend/internal/setting.js new file mode 100644 index 000000000..d4ac67d8a --- /dev/null +++ b/backend/internal/setting.js @@ -0,0 +1,133 @@ +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/stream.js b/backend/internal/stream.js new file mode 100644 index 000000000..50ce08324 --- /dev/null +++ b/backend/internal/stream.js @@ -0,0 +1,424 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +const streamModel = require('../models/stream'); +const internalNginx = require('./nginx'); +const internalAuditLog = require('./audit-log'); +const internalCertificate = require('./certificate'); +const internalHost = require('./host'); +const {castJsonIfNeed} = require('../lib/helpers'); + +function omissions () { + return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted']; +} + +const internalStream = { + + /** + * @param {Access} access + * @param {Object} data + * @returns {Promise} + */ + create: (access, data) => { + const create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + + 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 = {}; + } + + // streams aren't routed by domain name so don't store domain names in the DB + let data_no_domains = structuredClone(data); + delete data_no_domains.domain_names; + + return streamModel + .query() + .insertAndFetch(data_no_domains) + .then(utils.omitRow(omissions())); + }) + .then((row) => { + if (create_certificate) { + return internalCertificate.createQuickCertificate(access, data) + .then((cert) => { + // update host with cert id + return internalStream.update(access, { + id: row.id, + certificate_id: cert.id + }); + }) + .then(() => { + return row; + }); + } else { + return row; + } + }) + .then((row) => { + // re-fetch with cert + return internalStream.get(access, { + id: row.id, + expand: ['certificate', 'owner'] + }); + }) + .then((row) => { + // Configure nginx + return internalNginx.configure(streamModel, 'stream', row) + .then(() => { + return row; + }); + }) + .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) => { + const create_certificate = data.certificate_id === 'new'; + + if (create_certificate) { + delete data.certificate_id; + } + + 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); + } + + 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); + + return streamModel + .query() + .patchAndFetchById(row.id, data) + .then(utils.omitRow(omissions())) + .then((saved_row) => { + // Add to audit log + return internalAuditLog.add(access, { + action: 'updated', + object_type: 'stream', + object_id: row.id, + meta: data + }) + .then(() => { + return saved_row; + }); + }); + }) + .then(() => { + return internalStream.get(access, {id: data.id, expand: ['owner', 'certificate']}) + .then((row) => { + return internalNginx.configure(streamModel, 'stream', 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('streams:get', data.id) + .then((access_data) => { + let query = streamModel + .query() + .where('is_deleted', 0) + .andWhere('id', data.id) + .allowGraph('[owner,certificate]') + .first(); + + if (access_data.permission_visibility !== 'all') { + query.andWhere('owner_user_id', access.token.getUserId(1)); + } + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + row = internalHost.cleanRowCertificateMeta(row); + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * @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 || !row.id) { + 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: ['certificate', 'owner'] + }); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } else if (row.enabled) { + throw new error.ValidationError('Stream 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 || !row.id) { + throw new error.ItemNotFoundError(data.id); + } else if (!row.enabled) { + throw new error.ValidationError('Stream 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) => { + const query = streamModel + .query() + .where('is_deleted', 0) + .groupBy('id') + .allowGraph('[owner,certificate]') + .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' && search_query.length > 0) { + query.where(function () { + this.where(castJsonIfNeed('incoming_port'), 'like', `%${search_query}%`); + }); + } + + if (typeof expand !== 'undefined' && expand !== null) { + query.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }) + .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) => { + const query = streamModel + .query() + .count('id AS count') + .where('is_deleted', 0); + + if (visibility !== 'all') { + query.andWhere('owner_user_id', user_id); + } + + return query.first() + .then((row) => { + return parseInt(row.count, 10); + }); + } +}; + +module.exports = internalStream; diff --git a/backend/internal/token.js b/backend/internal/token.js new file mode 100644 index 000000000..0e6dec5e3 --- /dev/null +++ b/backend/internal/token.js @@ -0,0 +1,164 @@ +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'); + +const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password'; + +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.toLowerCase().trim()) + .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(ERROR_MESSAGE_INVALID_AUTH); + } + }); + } else { + throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH); + } + }); + } else { + throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH); + } + }); + }, + + /** + * @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/user.js b/backend/internal/user.js new file mode 100644 index 000000000..742ab65d3 --- /dev/null +++ b/backend/internal/user.js @@ -0,0 +1,513 @@ +const _ = require('lodash'); +const error = require('../lib/error'); +const utils = require('../lib/utils'); +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() + .insertAndFetch(data) + .then(utils.omitRow(omissions())); + }) + .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() + .patchAndFetchById(user.id, data) + .then(utils.omitRow(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) + .allowGraph('[permissions]') + .first(); + + if (typeof data.expand !== 'undefined' && data.expand !== null) { + query.withGraphFetched('[' + data.expand.join(', ') + ']'); + } + + return query.then(utils.omitRow(omissions())); + }) + .then((row) => { + if (!row || !row.id) { + throw new error.ItemNotFoundError(data.id); + } + // Custom omissions + if (typeof data.omit !== 'undefined' && data.omit !== null) { + row = _.omit(row, data.omit); + } + return row; + }); + }, + + /** + * 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') + .allowGraph('[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.withGraphFetched('[' + expand.join(', ') + ']'); + } + + return query.then(utils.omitRows(omissions())); + }); + }, + + /** + * @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/knexfile.js b/backend/knexfile.js new file mode 100644 index 000000000..607552f6f --- /dev/null +++ b/backend/knexfile.js @@ -0,0 +1,19 @@ +module.exports = { + development: { + client: 'mysql2', + migrations: { + tableName: 'migrations', + stub: 'lib/migrate_template.js', + directory: 'migrations' + } + }, + + production: { + client: 'mysql2', + migrations: { + tableName: 'migrations', + stub: 'lib/migrate_template.js', + directory: 'migrations' + } + } +}; diff --git a/backend/lib/access.js b/backend/lib/access.js new file mode 100644 index 000000000..0e658a656 --- /dev/null +++ b/backend/lib/access.js @@ -0,0 +1,307 @@ +/** + * 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 Ajv = require('ajv/dist/2020'); +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) + .allowGraph('[permissions]') + .withGraphFetched('[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', + 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) => { + const 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 = { + $async: true, + $id: 'permissions', + type: 'object', + additionalProperties: false, + properties: {} + }; + + permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json'); + + const ajv = new Ajv({ + verbose: true, + allErrors: true, + 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 new file mode 100644 index 000000000..5a16a8642 --- /dev/null +++ b/backend/lib/access/access_lists-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..5a16a8642 --- /dev/null +++ b/backend/lib/access/access_lists-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..8f6dd8cc6 --- /dev/null +++ b/backend/lib/access/access_lists-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..8f6dd8cc6 --- /dev/null +++ b/backend/lib/access/access_lists-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..5a16a8642 --- /dev/null +++ b/backend/lib/access/access_lists-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/auditlog-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/certificates-create.json b/backend/lib/access/certificates-create.json new file mode 100644 index 000000000..bcdf66742 --- /dev/null +++ b/backend/lib/access/certificates-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..bcdf66742 --- /dev/null +++ b/backend/lib/access/certificates-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..9ccfa4f15 --- /dev/null +++ b/backend/lib/access/certificates-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..9ccfa4f15 --- /dev/null +++ b/backend/lib/access/certificates-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..bcdf66742 --- /dev/null +++ b/backend/lib/access/certificates-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..a276c681d --- /dev/null +++ b/backend/lib/access/dead_hosts-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..a276c681d --- /dev/null +++ b/backend/lib/access/dead_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..87aa12e7d --- /dev/null +++ b/backend/lib/access/dead_hosts-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..87aa12e7d --- /dev/null +++ b/backend/lib/access/dead_hosts-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..a276c681d --- /dev/null +++ b/backend/lib/access/dead_hosts-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..e7a82ece3 --- /dev/null +++ b/backend/lib/access/permissions.json @@ -0,0 +1,13 @@ +{ + "$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 new file mode 100644 index 000000000..166527a39 --- /dev/null +++ b/backend/lib/access/proxy_hosts-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..166527a39 --- /dev/null +++ b/backend/lib/access/proxy_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..d88e4cfff --- /dev/null +++ b/backend/lib/access/proxy_hosts-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..d88e4cfff --- /dev/null +++ b/backend/lib/access/proxy_hosts-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..166527a39 --- /dev/null +++ b/backend/lib/access/proxy_hosts-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..342babc88 --- /dev/null +++ b/backend/lib/access/redirection_hosts-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..342babc88 --- /dev/null +++ b/backend/lib/access/redirection_hosts-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..ba2292064 --- /dev/null +++ b/backend/lib/access/redirection_hosts-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..ba2292064 --- /dev/null +++ b/backend/lib/access/redirection_hosts-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..342babc88 --- /dev/null +++ b/backend/lib/access/redirection_hosts-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..dbc9e0c0f --- /dev/null +++ b/backend/lib/access/reports-hosts.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/user" + } + ] +} diff --git a/backend/lib/access/roles.json b/backend/lib/access/roles.json new file mode 100644 index 000000000..c97313da8 --- /dev/null +++ b/backend/lib/access/roles.json @@ -0,0 +1,38 @@ +{ + "$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 new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/settings-get.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/settings-list.json b/backend/lib/access/settings-list.json new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/settings-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/settings-update.json b/backend/lib/access/settings-update.json new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/settings-update.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/streams-create.json b/backend/lib/access/streams-create.json new file mode 100644 index 000000000..fbeb1cc91 --- /dev/null +++ b/backend/lib/access/streams-create.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..fbeb1cc91 --- /dev/null +++ b/backend/lib/access/streams-delete.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..7e9962874 --- /dev/null +++ b/backend/lib/access/streams-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..7e9962874 --- /dev/null +++ b/backend/lib/access/streams-list.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..fbeb1cc91 --- /dev/null +++ b/backend/lib/access/streams-update.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/users-create.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-delete.json b/backend/lib/access/users-delete.json new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/users-delete.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-get.json b/backend/lib/access/users-get.json new file mode 100644 index 000000000..2a2f0423a --- /dev/null +++ b/backend/lib/access/users-get.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/users-list.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-loginas.json b/backend/lib/access/users-loginas.json new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/users-loginas.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-password.json b/backend/lib/access/users-password.json new file mode 100644 index 000000000..2a2f0423a --- /dev/null +++ b/backend/lib/access/users-password.json @@ -0,0 +1,23 @@ +{ + "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 new file mode 100644 index 000000000..aeadc94ba --- /dev/null +++ b/backend/lib/access/users-permissions.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/backend/lib/access/users-update.json b/backend/lib/access/users-update.json new file mode 100644 index 000000000..2a2f0423a --- /dev/null +++ b/backend/lib/access/users-update.json @@ -0,0 +1,23 @@ +{ + "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/certbot.js b/backend/lib/certbot.js new file mode 100644 index 000000000..96d947102 --- /dev/null +++ b/backend/lib/certbot.js @@ -0,0 +1,85 @@ +const dnsPlugins = require('../global/certbot-dns-plugins.json'); +const utils = require('./utils'); +const error = require('./error'); +const logger = require('../logger').certbot; +const batchflow = require('batchflow'); + +const CERTBOT_VERSION_REPLACEMENT = '$(certbot --version | grep -Eo \'[0-9](\\.[0-9]+)+\')'; + +const certbot = { + + /** + * @param {array} pluginKeys + */ + installPlugins: async (pluginKeys) => { + let hasErrors = false; + + return new Promise((resolve, reject) => { + if (pluginKeys.length === 0) { + resolve(); + return; + } + + batchflow(pluginKeys).sequential() + .each((_i, pluginKey, next) => { + certbot.installPlugin(pluginKey) + .then(() => { + next(); + }) + .catch((err) => { + hasErrors = true; + next(err); + }); + }) + .error((err) => { + logger.error(err.message); + }) + .end(() => { + if (hasErrors) { + reject(new error.CommandError('Some plugins failed to install. Please check the logs above', 1)); + } else { + resolve(); + } + }); + }); + }, + + /** + * Installs a cerbot plugin given the key for the object from + * ../global/certbot-dns-plugins.json + * + * @param {string} pluginKey + * @returns {Object} + */ + installPlugin: async (pluginKey) => { + if (typeof dnsPlugins[pluginKey] === 'undefined') { + // throw Error(`Certbot plugin ${pluginKey} not found`); + throw new error.ItemNotFoundError(pluginKey); + } + + const plugin = dnsPlugins[pluginKey]; + logger.start(`Installing ${pluginKey}...`); + + plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); + plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT); + + // SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly + // in new versions of Python + let env = Object.assign({}, process.env, {SETUPTOOLS_USE_DISTUTILS: 'stdlib'}); + if (typeof plugin.env === 'object') { + env = Object.assign(env, plugin.env); + } + + const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`; + return utils.exec(cmd, {env}) + .then((result) => { + logger.complete(`Installed ${pluginKey}`); + return result; + }) + .catch((err) => { + throw err; + }); + }, +}; + +module.exports = certbot; diff --git a/backend/lib/config.js b/backend/lib/config.js new file mode 100644 index 000000000..23184f3e8 --- /dev/null +++ b/backend/lib/config.js @@ -0,0 +1,237 @@ +const fs = require('fs'); +const NodeRSA = require('node-rsa'); +const logger = require('../logger').global; + +const keysFile = '/data/keys.json'; +const mysqlEngine = 'mysql2'; +const postgresEngine = 'pg'; +const sqliteClientName = 'sqlite3'; + +let instance = null; + +// 1. Load from config file first (not recommended anymore) +// 2. Use config env variables next +const configure = () => { + const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json'; + if (fs.existsSync(filename)) { + let configData; + try { + configData = require(filename); + } catch (_) { + // do nothing + } + + if (configData && configData.database) { + logger.info(`Using configuration from file: ${filename}`); + instance = configData; + instance.keys = getKeys(); + return; + } + } + + const envMysqlHost = process.env.DB_MYSQL_HOST || null; + const envMysqlUser = process.env.DB_MYSQL_USER || null; + const envMysqlName = process.env.DB_MYSQL_NAME || null; + if (envMysqlHost && envMysqlUser && envMysqlName) { + // we have enough mysql creds to go with mysql + logger.info('Using MySQL configuration'); + instance = { + database: { + engine: mysqlEngine, + host: envMysqlHost, + port: process.env.DB_MYSQL_PORT || 3306, + user: envMysqlUser, + password: process.env.DB_MYSQL_PASSWORD, + name: envMysqlName, + }, + keys: getKeys(), + }; + return; + } + + const envPostgresHost = process.env.DB_POSTGRES_HOST || null; + const envPostgresUser = process.env.DB_POSTGRES_USER || null; + const envPostgresName = process.env.DB_POSTGRES_NAME || null; + if (envPostgresHost && envPostgresUser && envPostgresName) { + // we have enough postgres creds to go with postgres + logger.info('Using Postgres configuration'); + instance = { + database: { + engine: postgresEngine, + host: envPostgresHost, + port: process.env.DB_POSTGRES_PORT || 5432, + user: envPostgresUser, + password: process.env.DB_POSTGRES_PASSWORD, + name: envPostgresName, + }, + keys: getKeys(), + }; + return; + } + + const envSqliteFile = process.env.DB_SQLITE_FILE || '/data/database.sqlite'; + logger.info(`Using Sqlite: ${envSqliteFile}`); + instance = { + database: { + engine: 'knex-native', + knex: { + client: sqliteClientName, + connection: { + filename: envSqliteFile + }, + useNullAsDefault: true + } + }, + keys: getKeys(), + }; +}; + +const getKeys = () => { + // Get keys from file + if (!fs.existsSync(keysFile)) { + generateKeys(); + } else if (process.env.DEBUG) { + logger.info('Keys file exists OK'); + } + try { + return require(keysFile); + } catch (err) { + logger.error('Could not read JWT key pair from config file: ' + keysFile, err); + process.exit(1); + } +}; + +const generateKeys = () => { + logger.info('Creating a new JWT key pair...'); + // Now create the keys and save them in the config. + const key = new NodeRSA({ b: 2048 }); + key.generateKeyPair(); + + const keys = { + key: key.exportKey('private').toString(), + pub: key.exportKey('public').toString(), + }; + + // Write keys config + try { + fs.writeFileSync(keysFile, JSON.stringify(keys, null, 2)); + } catch (err) { + logger.error('Could not write JWT key pair to config file: ' + keysFile + ': ' + err.message); + process.exit(1); + } + logger.info('Wrote JWT key pair to config file: ' + keysFile); +}; + +module.exports = { + + /** + * + * @param {string} key ie: 'database' or 'database.engine' + * @returns {boolean} + */ + has: function(key) { + instance === null && configure(); + const keys = key.split('.'); + let level = instance; + let has = true; + keys.forEach((keyItem) =>{ + if (typeof level[keyItem] === 'undefined') { + has = false; + } else { + level = level[keyItem]; + } + }); + + return has; + }, + + /** + * Gets a specific key from the top level + * + * @param {string} key + * @returns {*} + */ + get: function (key) { + instance === null && configure(); + if (key && typeof instance[key] !== 'undefined') { + return instance[key]; + } + return instance; + }, + + /** + * Is this a sqlite configuration? + * + * @returns {boolean} + */ + isSqlite: function () { + instance === null && configure(); + return instance.database.knex && instance.database.knex.client === sqliteClientName; + }, + + /** + * Is this a mysql configuration? + * + * @returns {boolean} + */ + isMysql: function () { + instance === null && configure(); + return instance.database.engine === mysqlEngine; + }, + + /** + * Is this a postgres configuration? + * + * @returns {boolean} + */ + isPostgres: function () { + instance === null && configure(); + return instance.database.engine === postgresEngine; + }, + + /** + * Are we running in debug mdoe? + * + * @returns {boolean} + */ + debug: function () { + return !!process.env.DEBUG; + }, + + /** + * Returns a public key + * + * @returns {string} + */ + getPublicKey: function () { + instance === null && configure(); + return instance.keys.pub; + }, + + /** + * Returns a private key + * + * @returns {string} + */ + getPrivateKey: function () { + instance === null && configure(); + return instance.keys.key; + }, + + /** + * @returns {boolean} + */ + useLetsencryptStaging: function () { + return !!process.env.LE_STAGING; + }, + + /** + * @returns {string|null} + */ + useLetsencryptServer: function () { + if (process.env.LE_SERVER) { + return process.env.LE_SERVER; + } + return null; + } +}; diff --git a/backend/lib/error.js b/backend/lib/error.js new file mode 100644 index 000000000..413d6a7d9 --- /dev/null +++ b/backend/lib/error.js @@ -0,0 +1,99 @@ +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; + }, + + CommandError: function (stdErr, code, previous) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = stdErr; + this.code = code; + this.public = false; + }, +}; + +_.forEach(module.exports, function (error) { + util.inherits(error, Error); +}); diff --git a/backend/lib/express/cors.js b/backend/lib/express/cors.js new file mode 100644 index 000000000..6d5b8b5fb --- /dev/null +++ b/backend/lib/express/cors.js @@ -0,0 +1,16 @@ +module.exports = function (req, res, next) { + if (req.headers.origin) { + 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(); + } else { + // No origin + next(); + } +}; diff --git a/backend/lib/express/jwt-decode.js b/backend/lib/express/jwt-decode.js new file mode 100644 index 000000000..17edccec0 --- /dev/null +++ b/backend/lib/express/jwt-decode.js @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..44aa36934 --- /dev/null +++ b/backend/lib/express/jwt.js @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..24ffa58d0 --- /dev/null +++ b/backend/lib/express/pagination.js @@ -0,0 +1,55 @@ +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 new file mode 100644 index 000000000..4a37a4069 --- /dev/null +++ b/backend/lib/express/user-id-from-me.js @@ -0,0 +1,9 @@ +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 new file mode 100644 index 000000000..ad3df3c27 --- /dev/null +++ b/backend/lib/helpers.js @@ -0,0 +1,62 @@ +const moment = require('moment'); +const {isPostgres} = require('./config'); +const {ref} = require('objection'); + +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; + }, + + convertIntFieldsToBool: function (obj, fields) { + fields.forEach(function (field) { + if (typeof obj[field] !== 'undefined') { + obj[field] = obj[field] === 1; + } + }); + return obj; + }, + + convertBoolFieldsToInt: function (obj, fields) { + fields.forEach(function (field) { + if (typeof obj[field] !== 'undefined') { + obj[field] = obj[field] ? 1 : 0; + } + }); + return obj; + }, + + /** + * Casts a column to json if using postgres + * + * @param {string} colName + * @returns {string|Objection.ReferenceBuilder} + */ + castJsonIfNeed: function (colName) { + return isPostgres() ? ref(colName).castText() : colName; + } + +}; diff --git a/backend/lib/migrate_template.js b/backend/lib/migrate_template.js new file mode 100644 index 000000000..f75f77ef4 --- /dev/null +++ b/backend/lib/migrate_template.js @@ -0,0 +1,55 @@ +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 new file mode 100644 index 000000000..66f2dfd95 --- /dev/null +++ b/backend/lib/utils.js @@ -0,0 +1,106 @@ +const _ = require('lodash'); +const exec = require('node:child_process').exec; +const execFile = require('node:child_process').execFile; +const { Liquid } = require('liquidjs'); +const logger = require('../logger').global; +const error = require('./error'); + +module.exports = { + + exec: async (cmd, options = {}) => { + logger.debug('CMD:', cmd); + + const { stdout, stderr } = await new Promise((resolve, reject) => { + const child = exec(cmd, options, (isError, stdout, stderr) => { + if (isError) { + reject(new error.CommandError(stderr, isError)); + } else { + resolve({ stdout, stderr }); + } + }); + + child.on('error', (e) => { + reject(new error.CommandError(stderr, 1, e)); + }); + }); + return stdout; + }, + + /** + * @param {String} cmd + * @param {Array} args + * @returns {Promise} + */ + execFile: (cmd, args) => { + // logger.debug('CMD: ' + cmd + ' ' + (args ? args.join(' ') : '')); + + return new Promise((resolve, reject) => { + execFile(cmd, args, (err, stdout, /*stderr*/) => { + if (err && typeof err === 'object') { + reject(err); + } else { + resolve(stdout.trim()); + } + }); + }); + }, + + /** + * Used in objection query builder + * + * @param {Array} omissions + * @returns {Function} + */ + omitRow: (omissions) => { + /** + * @param {Object} row + * @returns {Object} + */ + return (row) => { + return _.omit(row, omissions); + }; + }, + + /** + * Used in objection query builder + * + * @param {Array} omissions + * @returns {Function} + */ + omitRows: (omissions) => { + /** + * @param {Array} rows + * @returns {Object} + */ + return (rows) => { + rows.forEach((row, idx) => { + rows[idx] = _.omit(row, omissions); + }); + return rows; + }; + }, + + /** + * @returns {Object} Liquid render engine + */ + getRenderEngine: () => { + const renderEngine = new Liquid({ + root: `${__dirname}/../templates/` + }); + + /** + * nginxAccessRule expects the object given to have 2 properties: + * + * directive string + * address string + */ + renderEngine.registerFilter('nginxAccessRule', (v) => { + if (typeof v.directive !== 'undefined' && typeof v.address !== 'undefined' && v.directive && v.address) { + return `${v.directive} ${v.address};`; + } + return ''; + }); + + return renderEngine; + } +}; diff --git a/backend/lib/validator/api.js b/backend/lib/validator/api.js new file mode 100644 index 000000000..fb31e64c6 --- /dev/null +++ b/backend/lib/validator/api.js @@ -0,0 +1,43 @@ +const Ajv = require('ajv/dist/2020'); +const error = require('../error'); + +const ajv = new Ajv({ + verbose: true, + allErrors: true, + allowUnionTypes: true, + strict: false, + coerceTypes: true, +}); + +/** + * @param {Object} schema + * @param {Object} payload + * @returns {Promise} + */ +function apiValidator (schema, payload/*, description*/) { + return new Promise(function Promise_apiValidator (resolve, reject) { + if (schema === null) { + reject(new error.ValidationError('Schema is undefined')); + return; + } + + if (typeof payload === 'undefined') { + reject(new error.ValidationError('Payload is undefined')); + return; + } + + const validate = ajv.compile(schema); + const 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); + } + }); +} + +module.exports = apiValidator; diff --git a/backend/lib/validator/index.js b/backend/lib/validator/index.js new file mode 100644 index 000000000..c6d240967 --- /dev/null +++ b/backend/lib/validator/index.js @@ -0,0 +1,45 @@ +const _ = require('lodash'); +const Ajv = require('ajv/dist/2020'); +const error = require('../error'); +const commonDefinitions = require('../../schema/common.json'); + +RegExp.prototype.toJSON = RegExp.prototype.toString; + +const ajv = new Ajv({ + verbose: true, + allErrors: true, + allowUnionTypes: true, + coerceTypes: true, + strict: false, + schemas: [commonDefinitions] +}); + +/** + * + * @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 new file mode 100644 index 000000000..0ebb07c58 --- /dev/null +++ b/backend/logger.js @@ -0,0 +1,14 @@ +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 '}), + certbot: new Signale({scope: 'Certbot '}), + 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 new file mode 100644 index 000000000..263c87020 --- /dev/null +++ b/backend/migrate.js @@ -0,0 +1,15 @@ +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 new file mode 100644 index 000000000..a112e8261 --- /dev/null +++ b/backend/migrations/20180618015850_initial.js @@ -0,0 +1,205 @@ +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 new file mode 100644 index 000000000..060548502 --- /dev/null +++ b/backend/migrations/20180929054513_websockets.js @@ -0,0 +1,35 @@ +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/src/backend/migrations/20181019052346_forward_host.js b/backend/migrations/20181019052346_forward_host.js similarity index 50% rename from src/backend/migrations/20181019052346_forward_host.js rename to backend/migrations/20181019052346_forward_host.js index 0a8a4311c..05c277396 100644 --- a/src/backend/migrations/20181019052346_forward_host.js +++ b/backend/migrations/20181019052346_forward_host.js @@ -11,14 +11,14 @@ const logger = require('../logger').migrate; * @returns {Promise} */ exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); + 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'); - }); + 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'); + }); }; /** @@ -29,6 +29,6 @@ exports.up = function (knex/*, Promise*/) { * @returns {Promise} */ exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); + 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 new file mode 100644 index 000000000..9f6b43367 --- /dev/null +++ b/backend/migrations/20181113041458_http2_support.js @@ -0,0 +1,49 @@ +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/src/backend/migrations/20181213013211_forward_scheme.js b/backend/migrations/20181213013211_forward_scheme.js similarity index 50% rename from src/backend/migrations/20181213013211_forward_scheme.js rename to backend/migrations/20181213013211_forward_scheme.js index 4c02273de..22ae619ed 100644 --- a/src/backend/migrations/20181213013211_forward_scheme.js +++ b/backend/migrations/20181213013211_forward_scheme.js @@ -11,14 +11,14 @@ const logger = require('../logger').migrate; * @returns {Promise} */ exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); + 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'); - }); + 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'); + }); }; /** @@ -29,6 +29,6 @@ exports.up = function (knex/*, Promise*/) { * @returns {Promise} */ exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); + 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 new file mode 100644 index 000000000..2780c4df2 --- /dev/null +++ b/backend/migrations/20190104035154_disabled.js @@ -0,0 +1,55 @@ +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/src/backend/migrations/20190215115310_customlocations.js b/backend/migrations/20190215115310_customlocations.js similarity index 55% rename from src/backend/migrations/20190215115310_customlocations.js rename to backend/migrations/20190215115310_customlocations.js index b1766ac15..4bcfd51ad 100644 --- a/src/backend/migrations/20190215115310_customlocations.js +++ b/backend/migrations/20190215115310_customlocations.js @@ -12,14 +12,14 @@ const logger = require('../logger').migrate; * @returns {Promise} */ exports.up = function (knex/*, Promise*/) { - logger.info('[' + migrate_name + '] Migrating Up...'); + 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'); - }) + return knex.schema.table('proxy_host', function (proxy_host) { + proxy_host.json('locations'); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table altered'); + }); }; /** @@ -30,6 +30,6 @@ exports.up = function (knex/*, Promise*/) { * @returns {Promise} */ exports.down = function (knex, Promise) { - logger.warn('[' + migrate_name + '] You can\'t migrate down this one.'); - return Promise.resolve(true); + 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 new file mode 100644 index 000000000..648b162a0 --- /dev/null +++ b/backend/migrations/20190218060101_hsts.js @@ -0,0 +1,51 @@ +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 new file mode 100644 index 000000000..7dc9c1928 --- /dev/null +++ b/backend/migrations/20190227065017_settings.js @@ -0,0 +1,38 @@ +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 new file mode 100644 index 000000000..3511e35b3 --- /dev/null +++ b/backend/migrations/20200410143839_access_list_client.js @@ -0,0 +1,53 @@ +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 new file mode 100644 index 000000000..ee0f0906f --- /dev/null +++ b/backend/migrations/20200410143840_access_list_client_fix.js @@ -0,0 +1,34 @@ +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 new file mode 100644 index 000000000..a7767eb19 --- /dev/null +++ b/backend/migrations/20201014143841_pass_auth.js @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..0dad48768 --- /dev/null +++ b/backend/migrations/20210210154702_redirection_scheme.js @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..b9bea0b92 --- /dev/null +++ b/backend/migrations/20210210154703_redirection_status_code.js @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..a894ca5e6 --- /dev/null +++ b/backend/migrations/20210423103500_stream_domain.js @@ -0,0 +1,40 @@ +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 new file mode 100644 index 000000000..4c50941ff --- /dev/null +++ b/backend/migrations/20211108145214_regenerate_default_host.js @@ -0,0 +1,50 @@ +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/migrations/20240427161436_stream_ssl.js b/backend/migrations/20240427161436_stream_ssl.js new file mode 100644 index 000000000..5f47b18ec --- /dev/null +++ b/backend/migrations/20240427161436_stream_ssl.js @@ -0,0 +1,38 @@ +const migrate_name = 'stream_ssl'; +const logger = require('../logger').migrate; + +/** + * Migrate + * + * @see http://knexjs.org/#Schema + * + * @param {Object} knex + * @returns {Promise} + */ +exports.up = function (knex) { + logger.info('[' + migrate_name + '] Migrating Up...'); + + return knex.schema.table('stream', (table) => { + table.integer('certificate_id').notNull().unsigned().defaultTo(0); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; + +/** + * Undo Migrate + * + * @param {Object} knex + * @returns {Promise} + */ +exports.down = function (knex) { + logger.info('[' + migrate_name + '] Migrating Down...'); + + return knex.schema.table('stream', (table) => { + table.dropColumn('certificate_id'); + }) + .then(function () { + logger.info('[' + migrate_name + '] stream Table altered'); + }); +}; diff --git a/backend/models/access_list.js b/backend/models/access_list.js new file mode 100644 index 000000000..959df05f3 --- /dev/null +++ b/backend/models/access_list.js @@ -0,0 +1,103 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +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); + +const boolFields = [ + 'is_deleted', + 'satisfy_any', + 'pass_auth', +]; + +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(); + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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); + } + }, + items: { + relation: Model.HasManyRelation, + modelClass: AccessListAuth, + join: { + from: 'access_list.id', + to: 'access_list_auth.access_list_id' + } + }, + clients: { + relation: Model.HasManyRelation, + modelClass: AccessListClient, + join: { + from: 'access_list.id', + to: 'access_list_client.access_list_id' + } + }, + 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); + } + } + }; + } +} + +module.exports = AccessList; diff --git a/backend/models/access_list_auth.js b/backend/models/access_list_auth.js new file mode 100644 index 000000000..3895539c2 --- /dev/null +++ b/backend/models/access_list_auth.js @@ -0,0 +1,54 @@ +// 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); + } + } + }; + } +} + +module.exports = AccessListAuth; diff --git a/backend/models/access_list_client.js b/backend/models/access_list_client.js new file mode 100644 index 000000000..bffc00231 --- /dev/null +++ b/backend/models/access_list_client.js @@ -0,0 +1,54 @@ +// 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); + } + } + }; + } +} + +module.exports = AccessListClient; diff --git a/backend/models/audit-log.js b/backend/models/audit-log.js new file mode 100644 index 000000000..45a4b4602 --- /dev/null +++ b/backend/models/audit-log.js @@ -0,0 +1,52 @@ +// 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' + } + } + }; + } +} + +module.exports = AuditLog; diff --git a/backend/models/auth.js b/backend/models/auth.js new file mode 100644 index 000000000..469e96bf4 --- /dev/null +++ b/backend/models/auth.js @@ -0,0 +1,98 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const bcrypt = require('bcrypt'); +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const User = require('./user'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', +]; + +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); + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + /** + * 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 + } + } + }; + } +} + +module.exports = Auth; diff --git a/backend/models/certificate.js b/backend/models/certificate.js new file mode 100644 index 000000000..d4ea21ad5 --- /dev/null +++ b/backend/models/certificate.js @@ -0,0 +1,124 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', +]; + +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(); + } + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + static get name () { + return 'Certificate'; + } + + static get tableName () { + return 'certificate'; + } + + static get jsonAttributes () { + return ['domain_names', 'meta']; + } + + static get relationMappings () { + const ProxyHost = require('./proxy_host'); + const DeadHost = require('./dead_host'); + const User = require('./user'); + const RedirectionHost = require('./redirection_host'); + + 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); + } + }, + proxy_hosts: { + relation: Model.HasManyRelation, + modelClass: ProxyHost, + join: { + from: 'certificate.id', + to: 'proxy_host.certificate_id' + }, + modify: function (qb) { + qb.where('proxy_host.is_deleted', 0); + } + }, + dead_hosts: { + relation: Model.HasManyRelation, + modelClass: DeadHost, + join: { + from: 'certificate.id', + to: 'dead_host.certificate_id' + }, + modify: function (qb) { + qb.where('dead_host.is_deleted', 0); + } + }, + redirection_hosts: { + relation: Model.HasManyRelation, + modelClass: RedirectionHost, + join: { + from: 'certificate.id', + to: 'redirection_host.certificate_id' + }, + modify: function (qb) { + qb.where('redirection_host.is_deleted', 0); + } + } + }; + } +} + +module.exports = Certificate; diff --git a/backend/models/dead_host.js b/backend/models/dead_host.js new file mode 100644 index 000000000..3386caabf --- /dev/null +++ b/backend/models/dead_host.js @@ -0,0 +1,99 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const User = require('./user'); +const Certificate = require('./certificate'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', + 'ssl_forced', + 'http2_support', + 'enabled', + 'hsts_enabled', + 'hsts_subdomains', +]; + +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(); + } + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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); + } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'dead_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } + } + }; + } +} + +module.exports = DeadHost; diff --git a/backend/models/now_helper.js b/backend/models/now_helper.js new file mode 100644 index 000000000..dec70c3de --- /dev/null +++ b/backend/models/now_helper.js @@ -0,0 +1,13 @@ +const db = require('../db'); +const config = require('../lib/config'); +const Model = require('objection').Model; + +Model.knex(db); + +module.exports = function () { + if (config.isSqlite()) { + // eslint-disable-next-line + return Model.raw("datetime('now','localtime')"); + } + return Model.raw('NOW()'); +}; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js new file mode 100644 index 000000000..07aa5dd3c --- /dev/null +++ b/backend/models/proxy_host.js @@ -0,0 +1,114 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +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); + +const boolFields = [ + 'is_deleted', + 'ssl_forced', + 'caching_enabled', + 'block_exploits', + 'allow_websocket_upgrade', + 'http2_support', + 'enabled', + 'hsts_enabled', + 'hsts_subdomains', +]; + +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(); + } + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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); + } + }, + 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); + } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'proxy_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } + } + }; + } +} + +module.exports = ProxyHost; diff --git a/backend/models/redirection_host.js b/backend/models/redirection_host.js new file mode 100644 index 000000000..801627916 --- /dev/null +++ b/backend/models/redirection_host.js @@ -0,0 +1,102 @@ + +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const User = require('./user'); +const Certificate = require('./certificate'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', + 'enabled', + 'preserve_path', + 'ssl_forced', + 'block_exploits', + 'hsts_enabled', + 'hsts_subdomains', + 'http2_support', +]; + +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(); + } + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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); + } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'redirection_host.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } + } + }; + } +} + +module.exports = RedirectionHost; diff --git a/backend/models/setting.js b/backend/models/setting.js new file mode 100644 index 000000000..75aa90076 --- /dev/null +++ b/backend/models/setting.js @@ -0,0 +1,30 @@ +// 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 new file mode 100644 index 000000000..5d1cb6c1c --- /dev/null +++ b/backend/models/stream.js @@ -0,0 +1,82 @@ +const Model = require('objection').Model; +const db = require('../db'); +const helpers = require('../lib/helpers'); +const User = require('./user'); +const Certificate = require('./certificate'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', + 'enabled', + 'tcp_forwarding', + 'udp_forwarding', +]; + +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(); + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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); + } + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: 'stream.certificate_id', + to: 'certificate.id' + }, + modify: function (qb) { + qb.where('certificate.is_deleted', 0); + } + } + }; + } +} + +module.exports = Stream; diff --git a/backend/models/token.js b/backend/models/token.js new file mode 100644 index 000000000..7cf11e035 --- /dev/null +++ b/backend/models/token.js @@ -0,0 +1,139 @@ +/** + 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 config = require('../lib/config'); +const error = require('../lib/error'); +const logger = require('../logger').global; +const ALGO = 'RS256'; + +module.exports = function () { + + let token_data = {}; + + const self = { + /** + * @param {Object} payload + * @returns {Promise} + */ + create: (payload) => { + if (!config.getPrivateKey()) { + logger.error('Private key is empty!'); + } + // sign with RSA SHA256 + const options = { + algorithm: ALGO, + expiresIn: payload.expiresIn || '1d' + }; + + payload.jti = crypto.randomBytes(12) + .toString('base64') + .substring(-8); + + return new Promise((resolve, reject) => { + jwt.sign(payload, config.getPrivateKey(), options, (err, token) => { + if (err) { + reject(err); + } else { + token_data = payload; + resolve({ + token: token, + payload: payload + }); + } + }); + }); + }, + + /** + * @param {String} token + * @returns {Promise} + */ + load: function (token) { + if (!config.getPublicKey()) { + logger.error('Public key is empty!'); + } + return new Promise((resolve, reject) => { + try { + if (!token || token === null || token === 'null') { + reject(new error.AuthError('Empty token')); + } else { + jwt.verify(token, config.getPublicKey(), {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)) { + 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) => { + const 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 new file mode 100644 index 000000000..78fd3dd67 --- /dev/null +++ b/backend/models/user.js @@ -0,0 +1,69 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +const db = require('../db'); +const helpers = require('../lib/helpers'); +const Model = require('objection').Model; +const UserPermission = require('./user_permission'); +const now = require('./now_helper'); + +Model.knex(db); + +const boolFields = [ + 'is_deleted', + 'is_disabled', +]; + +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(); + } + + $parseDatabaseJson(json) { + json = super.$parseDatabaseJson(json); + return helpers.convertIntFieldsToBool(json, boolFields); + } + + $formatDatabaseJson(json) { + json = helpers.convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(json); + } + + 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' + } + } + }; + } + +} + +module.exports = User; diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js new file mode 100644 index 000000000..bb87d5dc4 --- /dev/null +++ b/backend/models/user_permission.js @@ -0,0 +1,29 @@ +// 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/nodemon.json b/backend/nodemon.json similarity index 60% rename from nodemon.json rename to backend/nodemon.json index bcbb216c3..3d6d13420 100644 --- a/nodemon.json +++ b/backend/nodemon.json @@ -1,9 +1,7 @@ { "verbose": false, "ignore": [ - "dist", - "data", - "src/frontend" + "data" ], "ext": "js json ejs" } diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 000000000..30984a332 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,48 @@ +{ + "name": "nginx-proxy-manager", + "version": "0.0.0", + "description": "A beautiful interface for creating Nginx endpoints", + "main": "index.js", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.7.0", + "ajv": "^8.17.1", + "archiver": "^5.3.0", + "batchflow": "^0.4.0", + "bcrypt": "^5.0.0", + "body-parser": "^1.20.3", + "compression": "^1.7.4", + "express": "^4.20.0", + "express-fileupload": "^1.1.9", + "gravatar": "^1.8.0", + "jsonwebtoken": "^9.0.0", + "knex": "2.4.2", + "liquidjs": "10.6.1", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "mysql2": "^3.11.1", + "node-rsa": "^1.0.8", + "objection": "3.0.1", + "path": "^0.12.7", + "pg": "^8.13.1", + "signale": "1.4.0", + "sqlite3": "5.1.6", + "temp-write": "^4.0.0" + }, + "signale": { + "displayDate": true, + "displayTimestamp": true + }, + "author": "Jamie Curnow ", + "license": "MIT", + "devDependencies": { + "@apidevtools/swagger-parser": "^10.1.0", + "chalk": "4.1.2", + "eslint": "^8.36.0", + "eslint-plugin-align-assignments": "^1.1.2", + "nodemon": "^2.0.2", + "prettier": "^2.0.4" + }, + "scripts": { + "validate-schema": "node validate-schema.js" + } +} diff --git a/backend/routes/audit-log.js b/backend/routes/audit-log.js new file mode 100644 index 000000000..c68c7b35b --- /dev/null +++ b/backend/routes/audit-log.js @@ -0,0 +1,52 @@ +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((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/audit-log + * + * Retrieve all logs + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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/src/backend/routes/api/main.js b/backend/routes/main.js similarity index 58% rename from src/backend/routes/api/main.js rename to backend/routes/main.js index bdbe08fa2..b97096d0e 100644 --- a/src/backend/routes/api/main.js +++ b/backend/routes/main.js @@ -1,11 +1,11 @@ const express = require('express'); -const pjson = require('../../../../package.json'); -const error = require('../../lib/error'); +const pjson = require('../package.json'); +const error = require('../lib/error'); let router = express.Router({ - caseSensitive: true, - strict: true, - mergeParams: true + caseSensitive: true, + strict: true, + mergeParams: true }); /** @@ -13,18 +13,19 @@ let router = express.Router({ * GET /api */ router.get('/', (req, res/*, next*/) => { - let version = pjson.version.split('-').shift().split('.'); + 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) - } - }); + 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')); @@ -42,9 +43,9 @@ router.use('/nginx/certificates', require('./nginx/certificates')); * * ALL /api/* */ -router.all(/(.+)/, function (req, res, next) { - req.params.page = req.params['0']; - next(new error.ItemNotFoundError(req.params.page)); +router.all(/(.+)/, function (req, _, next) { + req.params.page = req.params['0']; + next(new error.ItemNotFoundError(req.params.page)); }); module.exports = router; diff --git a/backend/routes/nginx/access_lists.js b/backend/routes/nginx/access_lists.js new file mode 100644 index 000000000..383751277 --- /dev/null +++ b/backend/routes/nginx/access_lists.js @@ -0,0 +1,149 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalAccessList = require('../../internal/access-list'); +const schema = require('../../schema'); + +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: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/access-lists', 'post'), 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((_, 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/access-lists/{listID}', 'put'), 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/nginx/certificates.js b/backend/routes/nginx/certificates.js new file mode 100644 index 000000000..bf47c03fc --- /dev/null +++ b/backend/routes/nginx/certificates.js @@ -0,0 +1,288 @@ +const express = require('express'); +const error = require('../../lib/error'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalCertificate = require('../../internal/certificate'); +const schema = require('../../schema'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/certificates + */ +router + .route('/') + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates + * + * Retrieve all certificates + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/certificates', 'post'), 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((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/nginx/certificates/test-http + * + * Test HTTP challenge for domains + */ + .get((req, res, next) => { + if (req.query.domains === undefined) { + next(new error.ValidationError('Domains are required as query parameters')); + return; + } + + 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((_, 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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); + }) + + /** + * 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((_, 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((_, 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((_, 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/nginx/dead_hosts.js b/backend/routes/nginx/dead_hosts.js new file mode 100644 index 000000000..83b377653 --- /dev/null +++ b/backend/routes/nginx/dead_hosts.js @@ -0,0 +1,197 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalDeadHost = require('../../internal/dead-host'); +const schema = require('../../schema'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/nginx/dead-hosts + */ +router + .route('/') + .options((_, 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: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/dead-hosts', 'post'), 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/dead-hosts/{hostID}', 'put'), 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((_, 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((_, 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/nginx/proxy_hosts.js b/backend/routes/nginx/proxy_hosts.js new file mode 100644 index 000000000..3be4582a8 --- /dev/null +++ b/backend/routes/nginx/proxy_hosts.js @@ -0,0 +1,197 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalProxyHost = require('../../internal/proxy-host'); +const schema = require('../../schema'); + +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: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/proxy-hosts', 'post'), 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/proxy-hosts/{hostID}', 'put'), 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((_, 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((_, 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/nginx/redirection_hosts.js b/backend/routes/nginx/redirection_hosts.js new file mode 100644 index 000000000..a46feb841 --- /dev/null +++ b/backend/routes/nginx/redirection_hosts.js @@ -0,0 +1,197 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalRedirectionHost = require('../../internal/redirection-host'); +const schema = require('../../schema'); + +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: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/redirection-hosts', 'post'), 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/redirection-hosts/{hostID}', 'put'), 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/nginx/streams.js b/backend/routes/nginx/streams.js new file mode 100644 index 000000000..c033f2ef1 --- /dev/null +++ b/backend/routes/nginx/streams.js @@ -0,0 +1,197 @@ +const express = require('express'); +const validator = require('../../lib/validator'); +const jwtdecode = require('../../lib/express/jwt-decode'); +const apiValidator = require('../../lib/validator/api'); +const internalStream = require('../../internal/stream'); +const schema = require('../../schema'); + +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: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/streams', 'post'), 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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(schema.getValidationSchema('/nginx/streams/{streamID}', 'put'), 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((_, 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((_, 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/reports.js b/backend/routes/reports.js new file mode 100644 index 000000000..98c6cf86d --- /dev/null +++ b/backend/routes/reports.js @@ -0,0 +1,29 @@ +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((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /reports/hosts + */ + .get(jwtdecode(), (_, res, next) => { + internalReport.getHostsReport(res.locals.access) + .then((data) => { + res.status(200) + .send(data); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/routes/schema.js b/backend/routes/schema.js new file mode 100644 index 000000000..fc3e48b6c --- /dev/null +++ b/backend/routes/schema.js @@ -0,0 +1,38 @@ +const express = require('express'); +const schema = require('../schema'); +const PACKAGE = require('../package.json'); + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +router + .route('/') + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /schema + */ + .get(async (req, res) => { + let swaggerJSON = await schema.getCompiledSchema(); + + 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/settings.js b/backend/routes/settings.js new file mode 100644 index 000000000..dac4c3d1a --- /dev/null +++ b/backend/routes/settings.js @@ -0,0 +1,98 @@ +const express = require('express'); +const validator = require('../lib/validator'); +const jwtdecode = require('../lib/express/jwt-decode'); +const apiValidator = require('../lib/validator/api'); +const internalSetting = require('../internal/setting'); +const schema = require('../schema'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/settings + */ +router + .route('/') + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/settings + * + * Retrieve all settings + */ + .get((_, 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((_, 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: { + type: 'string', + minLength: 1 + } + } + }, { + 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(schema.getValidationSchema('/settings/{settingID}', 'put'), 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/tokens.js b/backend/routes/tokens.js new file mode 100644 index 000000000..72d01d41d --- /dev/null +++ b/backend/routes/tokens.js @@ -0,0 +1,53 @@ +const express = require('express'); +const jwtdecode = require('../lib/express/jwt-decode'); +const apiValidator = require('../lib/validator/api'); +const internalToken = require('../internal/token'); +const schema = require('../schema'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +router + .route('/') + .options((_, 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(async (req, res, next) => { + apiValidator(schema.getValidationSchema('/tokens', 'post'), req.body) + .then(internalToken.getTokenFromEmail) + .then((data) => { + res.status(200) + .send(data); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/routes/users.js b/backend/routes/users.js new file mode 100644 index 000000000..e41bf6cfb --- /dev/null +++ b/backend/routes/users.js @@ -0,0 +1,247 @@ +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'); +const schema = require('../schema'); + +let router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true +}); + +/** + * /api/users + */ +router + .route('/') + .options((_, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) + + /** + * GET /api/users + * + * Retrieve all users + */ + .get((req, res, next) => { + validator({ + additionalProperties: false, + properties: { + expand: { + $ref: 'common#/properties/expand' + }, + query: { + $ref: 'common#/properties/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((err) => { + console.log(err); + next(err); + }); + //.catch(next); + }) + + /** + * POST /api/users + * + * Create a new User + */ + .post((req, res, next) => { + apiValidator(schema.getValidationSchema('/users', 'post'), 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((_, 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: 'common#/properties/id' + }, + expand: { + $ref: 'common#/properties/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((err) => { + console.log(err); + next(err); + }); + }) + + /** + * PUT /api/users/123 + * + * Update and existing user + */ + .put((req, res, next) => { + apiValidator(schema.getValidationSchema('/users/{userID}', 'put'), 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(schema.getValidationSchema('/users/{userID}/auth', 'put'), req.body) + .then((payload) => { + payload.id = req.params.user_id; + return internalUser.setPassword(res.locals.access, payload); + }) + .then((result) => { + res.status(200) + .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(schema.getValidationSchema('/users/{userID}/permissions', 'put'), req.body) + .then((payload) => { + payload.id = req.params.user_id; + return internalUser.setPermissions(res.locals.access, payload); + }) + .then((result) => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +/** + * Specific user login as + * + * /api/users/123/login + */ +router + .route('/:user_id/login') + .options((_, 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(200) + .send(result); + }) + .catch(next); + }); + +module.exports = router; diff --git a/backend/schema/common.json b/backend/schema/common.json new file mode 100644 index 000000000..83de0143c --- /dev/null +++ b/backend/schema/common.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "common", + "type": "object", + "properties": { + "id": { + "description": "Unique identifier", + "readOnly": true, + "type": "integer", + "minimum": 1 + }, + "expand": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + }, + "query": { + "anyOf": [ + { + "type": "null" + }, + { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + ] + }, + "created_on": { + "description": "Date and time of creation", + "readOnly": true, + "type": "string" + }, + "modified_on": { + "description": "Date and time of last update", + "readOnly": true, + "type": "string" + }, + "user_id": { + "description": "User ID", + "type": "integer", + "minimum": 1 + }, + "certificate_id": { + "description": "Certificate ID", + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^new$" + } + ] + }, + "access_list_id": { + "description": "Access List ID", + "type": "integer", + "minimum": 0 + }, + "domain_names": { + "description": "Domain Names separated by a comma", + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + } + }, + "enabled": { + "description": "Is Enabled", + "type": "boolean" + }, + "ssl_forced": { + "description": "Is SSL Forced", + "type": "boolean" + }, + "hsts_enabled": { + "description": "Is HSTS Enabled", + "type": "boolean" + }, + "hsts_subdomains": { + "description": "Is HSTS applicable to all subdomains", + "type": "boolean" + }, + "ssl_provider": { + "type": "string", + "pattern": "^(letsencrypt|other)$" + }, + "http2_support": { + "description": "HTTP2 Protocol Support", + "type": "boolean" + }, + "block_exploits": { + "description": "Should we block common exploits", + "type": "boolean" + }, + "caching_enabled": { + "description": "Should we cache assets", + "type": "boolean" + } + } +} diff --git a/backend/schema/components/access-list-object.json b/backend/schema/components/access-list-object.json new file mode 100644 index 000000000..cd0218d72 --- /dev/null +++ b/backend/schema/components/access-list-object.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "description": "Access List object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "name", "directive", "address", "satisfy_any", "pass_auth", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "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" + } + } +} diff --git a/backend/schema/components/audit-log-object.json b/backend/schema/components/audit-log-object.json new file mode 100644 index 000000000..3e5e8594b --- /dev/null +++ b/backend/schema/components/audit-log-object.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "description": "Audit Log object", + "required": ["id", "created_on", "modified_on", "user_id", "object_type", "object_id", "action", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "object_type": { + "type": "string" + }, + "object_id": { + "$ref": "../common.json#/properties/id" + }, + "action": { + "type": "string" + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/schema/components/certificate-list.json b/backend/schema/components/certificate-list.json new file mode 100644 index 000000000..cec4db82c --- /dev/null +++ b/backend/schema/components/certificate-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Certificates list", + "items": { + "$ref": "./certificate-object.json" + } +} diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json new file mode 100644 index 000000000..b75dcf61d --- /dev/null +++ b/backend/schema/components/certificate-object.json @@ -0,0 +1,81 @@ +{ + "type": "object", + "description": "Certificate object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "provider", "nice_name", "domain_names", "expires_on", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "provider": { + "$ref": "../common.json#/properties/ssl_provider" + }, + "nice_name": { + "type": "string", + "description": "Nice Name for the custom certificate" + }, + "domain_names": { + "description": "Domain Names separated by a comma", + "type": "array", + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + } + }, + "expires_on": { + "description": "Date and time of expiration", + "readOnly": true, + "type": "string" + }, + "owner": { + "$ref": "./user-object.json" + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "certificate": { + "type": "string", + "minLength": 1 + }, + "certificate_key": { + "type": "string", + "minLength": 1 + }, + "dns_challenge": { + "type": "boolean" + }, + "dns_provider": { + "type": "string" + }, + "dns_provider_credentials": { + "type": "string" + }, + "letsencrypt_agree": { + "type": "boolean" + }, + "letsencrypt_certificate": { + "type": "object" + }, + "letsencrypt_email": { + "type": "string" + }, + "propagation_seconds": { + "type": "integer", + "minimum": 0 + } + } + } + } +} diff --git a/backend/schema/components/dead-host-list.json b/backend/schema/components/dead-host-list.json new file mode 100644 index 000000000..56ff303ba --- /dev/null +++ b/backend/schema/components/dead-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "404 Hosts list", + "items": { + "$ref": "./dead-host-object.json" + } +} diff --git a/backend/schema/components/dead-host-object.json b/backend/schema/components/dead-host-object.json new file mode 100644 index 000000000..792c2f81e --- /dev/null +++ b/backend/schema/components/dead-host-object.json @@ -0,0 +1,47 @@ +{ + "type": "object", + "description": "404 Host object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "advanced_config", "enabled", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "advanced_config": { + "type": "string" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/schema/components/error-object.json b/backend/schema/components/error-object.json new file mode 100644 index 000000000..c2540cf1f --- /dev/null +++ b/backend/schema/components/error-object.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "description": "Error object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } +} diff --git a/backend/schema/components/error.json b/backend/schema/components/error.json new file mode 100644 index 000000000..ceb3e1492 --- /dev/null +++ b/backend/schema/components/error.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "description": "Error", + "properties": { + "error": { + "$ref": "./error-object.json" + } + } +} diff --git a/backend/schema/components/health-object.json b/backend/schema/components/health-object.json new file mode 100644 index 000000000..8d223417b --- /dev/null +++ b/backend/schema/components/health-object.json @@ -0,0 +1,38 @@ +{ + "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 + } + } + } + } +} diff --git a/backend/schema/components/permission-object.json b/backend/schema/components/permission-object.json new file mode 100644 index 000000000..b852a014d --- /dev/null +++ b/backend/schema/components/permission-object.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "minProperties": 1, + "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"] + } + } +} diff --git a/backend/schema/components/proxy-host-list.json b/backend/schema/components/proxy-host-list.json new file mode 100644 index 000000000..39789b4a7 --- /dev/null +++ b/backend/schema/components/proxy-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Proxy Hosts list", + "items": { + "$ref": "./proxy-host-object.json" + } +} diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json new file mode 100644 index 000000000..e9dcacb5e --- /dev/null +++ b/backend/schema/components/proxy-host-object.json @@ -0,0 +1,153 @@ +{ + "type": "object", + "description": "Proxy Host object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "domain_names", + "forward_host", + "forward_port", + "access_list_id", + "certificate_id", + "ssl_forced", + "caching_enabled", + "block_exploits", + "advanced_config", + "meta", + "allow_websocket_upgrade", + "http2_support", + "forward_scheme", + "enabled", + "locations", + "hsts_enabled", + "hsts_subdomains" + ], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "forward_host": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "forward_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "access_list_id": { + "$ref": "../common.json#/properties/access_list_id" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "caching_enabled": { + "$ref": "../common.json#/properties/caching_enabled" + }, + "block_exploits": { + "$ref": "../common.json#/properties/block_exploits" + }, + "advanced_config": { + "type": "string" + }, + "meta": { + "type": "object" + }, + "allow_websocket_upgrade": { + "description": "Allow Websocket Upgrade for all paths", + "example": true, + "type": "boolean" + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "forward_scheme": { + "type": "string", + "enum": ["http", "https"] + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "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": "#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "#/properties/forward_host" + }, + "forward_port": { + "$ref": "#/properties/forward_port" + }, + "forward_path": { + "type": "string" + }, + "advanced_config": { + "type": "string" + } + } + } + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "certificate": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./certificate-object.json" + } + ] + }, + "owner": { + "$ref": "./user-object.json" + }, + "access_list": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./access-list-object.json" + } + ] + } + } +} diff --git a/backend/schema/components/redirection-host-list.json b/backend/schema/components/redirection-host-list.json new file mode 100644 index 000000000..716dcfa1d --- /dev/null +++ b/backend/schema/components/redirection-host-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Redirection Hosts list", + "items": { + "$ref": "./redirection-host-object.json" + } +} diff --git a/backend/schema/components/redirection-host-object.json b/backend/schema/components/redirection-host-object.json new file mode 100644 index 000000000..e7a495fd3 --- /dev/null +++ b/backend/schema/components/redirection-host-object.json @@ -0,0 +1,72 @@ +{ + "type": "object", + "description": "Redirection Host object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "domain_names", "forward_http_code", "forward_scheme", "forward_domain_name", "preserve_path", "certificate_id", "ssl_forced", "hsts_enabled", "hsts_subdomains", "http2_support", "block_exploits", "advanced_config", "enabled", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "$ref": "../common.json#/properties/domain_names" + }, + "forward_http_code": { + "description": "Redirect HTTP Status Code", + "example": 302, + "type": "integer", + "minimum": 300, + "maximum": 308 + }, + "forward_scheme": { + "type": "string", + "enum": ["auto", "http", "https"] + }, + "forward_domain_name": { + "description": "Domain Name", + "example": "jc21.com", + "type": "string", + "pattern": "^(?:[^.*]+\\.?)+[^.]$" + }, + "preserve_path": { + "description": "Should the path be preserved", + "example": true, + "type": "boolean" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../common.json#/properties/block_exploits" + }, + "advanced_config": { + "type": "string" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "meta": { + "type": "object" + } + } +} diff --git a/backend/schema/components/security-schemes.json b/backend/schema/components/security-schemes.json new file mode 100644 index 000000000..82407be3f --- /dev/null +++ b/backend/schema/components/security-schemes.json @@ -0,0 +1,6 @@ +{ + "BearerAuth": { + "type": "http", + "scheme": "bearer" + } +} diff --git a/backend/schema/components/setting-list.json b/backend/schema/components/setting-list.json new file mode 100644 index 000000000..c66f099ea --- /dev/null +++ b/backend/schema/components/setting-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Setting list", + "items": { + "$ref": "./setting-object.json" + } +} diff --git a/backend/schema/components/setting-object.json b/backend/schema/components/setting-object.json new file mode 100644 index 000000000..b9c6a1039 --- /dev/null +++ b/backend/schema/components/setting-object.json @@ -0,0 +1,56 @@ +{ + "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", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "integer" + }, + { + "type": "object" + }, + { + "type": "number" + }, + { + "type": "array" + } + ] + }, + "meta": { + "description": "Extra metadata", + "example": { + "redirect": "http://example.com", + "html": "

404

" + }, + "type": "object" + } + } +} diff --git a/backend/schema/components/stream-list.json b/backend/schema/components/stream-list.json new file mode 100644 index 000000000..b6e8b6d44 --- /dev/null +++ b/backend/schema/components/stream-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "Streams list", + "items": { + "$ref": "./stream-object.json" + } +} diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json new file mode 100644 index 000000000..848c30e6e --- /dev/null +++ b/backend/schema/components/stream-object.json @@ -0,0 +1,76 @@ +{ + "type": "object", + "description": "Stream object", + "required": ["id", "created_on", "modified_on", "owner_user_id", "incoming_port", "forwarding_host", "forwarding_port", "tcp_forwarding", "udp_forwarding", "enabled", "meta"], + "additionalProperties": false, + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "incoming_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "forwarding_host": { + "anyOf": [ + { + "description": "Domain Name", + "example": "jc21.com", + "type": "string", + "pattern": "^(?:[^.*]+\\.?)+[^.]$" + }, + { + "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": "../common.json#/properties/enabled" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "meta": { + "type": "object" + }, + "owner": { + "$ref": "./user-object.json" + }, + "certificate": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./certificate-object.json" + } + ] + } + } +} diff --git a/backend/schema/components/token-object.json b/backend/schema/components/token-object.json new file mode 100644 index 000000000..6ec4e4348 --- /dev/null +++ b/backend/schema/components/token-object.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "description": "Token object", + "required": ["expires", "token"], + "additionalProperties": false, + "properties": { + "expires": { + "description": "Token Expiry ISO Time String", + "example": "2025-02-04T20:40:46.340Z", + "type": "string" + }, + "token": { + "description": "JWT Token", + "example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4", + "type": "string" + } + } +} diff --git a/backend/schema/components/user-list.json b/backend/schema/components/user-list.json new file mode 100644 index 000000000..c5c0f7116 --- /dev/null +++ b/backend/schema/components/user-list.json @@ -0,0 +1,7 @@ +{ + "type": "array", + "description": "User list", + "items": { + "$ref": "./user-object.json" + } +} diff --git a/backend/schema/components/user-object.json b/backend/schema/components/user-object.json new file mode 100644 index 000000000..180e8f197 --- /dev/null +++ b/backend/schema/components/user-object.json @@ -0,0 +1,59 @@ +{ + "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": "boolean", + "description": "Is user Disabled", + "example": true + }, + "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" + } + } + } +} diff --git a/backend/schema/index.js b/backend/schema/index.js new file mode 100644 index 000000000..87b75f257 --- /dev/null +++ b/backend/schema/index.js @@ -0,0 +1,41 @@ +const refParser = require('@apidevtools/json-schema-ref-parser'); + +let compiledSchema = null; + +module.exports = { + + /** + * Compiles the schema, by dereferencing it, only once + * and returns the memory cached value + */ + getCompiledSchema: async () => { + if (compiledSchema === null) { + compiledSchema = await refParser.dereference(__dirname + '/swagger.json', { + mutateInputSchema: false, + }); + } + return compiledSchema; + }, + + /** + * Scans the schema for the validation schema for the given path and method + * and returns it. + * + * @param {string} path + * @param {string} method + * @returns string|null + */ + getValidationSchema: (path, method) => { + if (compiledSchema !== null && + typeof compiledSchema.paths[path] !== 'undefined' && + typeof compiledSchema.paths[path][method] !== 'undefined' && + typeof compiledSchema.paths[path][method].requestBody !== 'undefined' && + typeof compiledSchema.paths[path][method].requestBody.content !== 'undefined' && + typeof compiledSchema.paths[path][method].requestBody.content['application/json'] !== 'undefined' && + typeof compiledSchema.paths[path][method].requestBody.content['application/json'].schema !== 'undefined' + ) { + return compiledSchema.paths[path][method].requestBody.content['application/json'].schema; + } + return null; + } +}; diff --git a/backend/schema/paths/audit-log/get.json b/backend/schema/paths/audit-log/get.json new file mode 100644 index 000000000..bc43e29dd --- /dev/null +++ b/backend/schema/paths/audit-log/get.json @@ -0,0 +1,53 @@ +{ + "operationId": "getAuditLog", + "summary": "Get Audit Log", + "tags": ["Audit Log"], + "security": [ + { + "BearerAuth": ["audit-log"] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 7, + "created_on": "2024-10-08T13:09:54.000Z", + "modified_on": "2024-10-08T13:09:54.000Z", + "user_id": 1, + "object_type": "user", + "object_id": 3, + "action": "updated", + "meta": { + "name": "John Doe", + "permissions": { + "user_id": 3, + "visibility": "all", + "access_lists": "manage", + "dead_hosts": "hidden", + "proxy_hosts": "manage", + "redirection_hosts": "view", + "streams": "hidden", + "certificates": "manage", + "id": 3, + "modified_on": "2024-10-08T13:09:54.000Z", + "created_on": "2024-10-08T13:09:51.000Z" + } + } + } + ] + } + }, + "schema": { + "$ref": "../../components/audit-log-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/get.json b/backend/schema/paths/get.json new file mode 100644 index 000000000..8c3a4e025 --- /dev/null +++ b/backend/schema/paths/get.json @@ -0,0 +1,29 @@ +{ + "operationId": "health", + "summary": "Returns the API health status", + "tags": ["Public"], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "status": "OK", + "version": { + "major": 2, + "minor": 1, + "revision": 0 + } + } + } + }, + "schema": { + "$ref": "../components/health-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/get.json b/backend/schema/paths/nginx/access-lists/get.json new file mode 100644 index 000000000..a8b9adc69 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/get.json @@ -0,0 +1,50 @@ +{ + "operationId": "getAccessLists", + "summary": "Get all access lists", + "tags": ["Access Lists"], + "security": [ + { + "BearerAuth": ["access_lists"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "items", "clients", "proxy_hosts"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "owner_user_id": 1, + "name": "test1234", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0 + } + ] + } + }, + "schema": { + "$ref": "../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/delete.json b/backend/schema/paths/nginx/access-lists/listID/delete.json new file mode 100644 index 000000000..073585c8b --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteAccessList", + "summary": "Delete a Access List", + "tags": ["Access Lists"], + "security": [ + { + "BearerAuth": ["access_lists"] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/get.json b/backend/schema/paths/nginx/access-lists/listID/get.json new file mode 100644 index 000000000..e67023f89 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/get.json @@ -0,0 +1,49 @@ +{ + "operationId": "getAccessList", + "summary": "Get a access List", + "tags": ["Access Lists"], + "security": [ + { + "BearerAuth": ["access_lists"] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "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": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + } + }, + "schema": { + "$ref": "../../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/listID/put.json b/backend/schema/paths/nginx/access-lists/listID/put.json new file mode 100644 index 000000000..7f887dad6 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/listID/put.json @@ -0,0 +1,163 @@ +{ + "operationId": "updateAccessList", + "summary": "Update a Access List", + "tags": ["Access Lists"], + "security": [ + { + "BearerAuth": ["access_lists"] + } + ], + "parameters": [ + { + "in": "path", + "name": "listID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Access List Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "$ref": "../../../../components/access-list-object.json#/properties/name" + }, + "satisfy_any": { + "$ref": "../../../../components/access-list-object.json#/properties/satisfy_any" + }, + "pass_auth": { + "$ref": "../../../../components/access-list-object.json#/properties/pass_auth" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string" + } + } + } + }, + "clients": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "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$" + } + ] + }, + "directive": { + "$ref": "../../../../components/access-list-object.json#/properties/directive" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:34:34.000Z", + "owner_user_id": 1, + "name": "test123!!", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": ["admin"] + }, + "items": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "admin", + "password": "", + "meta": {}, + "hint": "a****" + }, + { + "id": 2, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "asdad", + "password": "", + "meta": {}, + "hint": "a*****" + } + ], + "clients": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "address": "127.0.0.1", + "directive": "allow", + "meta": {} + } + ], + "proxy_hosts": [] + } + } + }, + "schema": { + "$ref": "../../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/access-lists/post.json b/backend/schema/paths/nginx/access-lists/post.json new file mode 100644 index 000000000..4c5a4edd2 --- /dev/null +++ b/backend/schema/paths/nginx/access-lists/post.json @@ -0,0 +1,155 @@ +{ + "operationId": "createAccessList", + "summary": "Create a Access List", + "tags": ["Access Lists"], + "security": [ + { + "BearerAuth": ["access_lists"] + } + ], + "requestBody": { + "description": "Access List Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "$ref": "../../../components/access-list-object.json#/properties/name" + }, + "satisfy_any": { + "$ref": "../../../components/access-list-object.json#/properties/satisfy_any" + }, + "pass_auth": { + "$ref": "../../../components/access-list-object.json#/properties/pass_auth" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "type": "string", + "minLength": 1 + } + } + } + }, + "clients": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "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$" + } + ] + }, + "directive": { + "$ref": "../../../components/access-list-object.json#/properties/directive" + } + } + } + }, + "meta": { + "$ref": "../../../components/access-list-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "owner_user_id": 1, + "name": "test1234", + "meta": {}, + "satisfy_any": true, + "pass_auth": false, + "proxy_host_count": 0, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": ["admin"] + }, + "items": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "admin", + "password": "", + "meta": {}, + "hint": "a****" + }, + { + "id": 2, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "username": "asdad", + "password": "", + "meta": {}, + "hint": "a*****" + } + ], + "proxy_hosts": [], + "clients": [ + { + "id": 1, + "created_on": "2024-10-08T22:15:40.000Z", + "modified_on": "2024-10-08T22:15:40.000Z", + "access_list_id": 1, + "address": "127.0.0.1", + "directive": "allow", + "meta": {} + } + ] + } + } + }, + "schema": { + "$ref": "../../../components/access-list-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/delete.json b/backend/schema/paths/nginx/certificates/certID/delete.json new file mode 100644 index 000000000..0d40bcb8c --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteCertificate", + "summary": "Delete a Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/download/get.json b/backend/schema/paths/nginx/certificates/certID/download/get.json new file mode 100644 index 000000000..4b858cae7 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/download/get.json @@ -0,0 +1,35 @@ +{ + "operationId": "downloadCertificate", + "summary": "Downloads a Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/get.json b/backend/schema/paths/nginx/certificates/certID/get.json new file mode 100644 index 000000000..22317b337 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/get.json @@ -0,0 +1,53 @@ +{ + "operationId": "getCertificate", + "summary": "Get a Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "expires_on": "2025-01-07T04:34:18.000Z", + "meta": { + "letsencrypt_email": "jc@jc21.com", + "letsencrypt_agree": true, + "dns_challenge": false + } + } + } + }, + "schema": { + "$ref": "../../../../components/certificate-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/renew/post.json b/backend/schema/paths/nginx/certificates/certID/renew/post.json new file mode 100644 index 000000000..ef4d20e5b --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/renew/post.json @@ -0,0 +1,54 @@ +{ + "operationId": "renewCertificate", + "summary": "Renews a Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires_on": "2025-01-07T06:41:58.000Z", + "modified_on": "2024-10-09T07:39:51.000Z", + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "owner_user_id": 1, + "is_deleted": false, + "provider": "letsencrypt", + "nice_name": "My Test Cert", + "domain_names": ["test.jc21.supernerd.pro"], + "meta": { + "letsencrypt_email": "jc@jc21.com", + "letsencrypt_agree": true, + "dns_challenge": false + } + } + } + }, + "schema": { + "$ref": "../../../../../components/certificate-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/certID/upload/post.json b/backend/schema/paths/nginx/certificates/certID/upload/post.json new file mode 100644 index 000000000..f38b8102a --- /dev/null +++ b/backend/schema/paths/nginx/certificates/certID/upload/post.json @@ -0,0 +1,82 @@ +{ + "operationId": "uploadCertificate", + "summary": "Uploads a custom Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "path", + "name": "certID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "requestBody": { + "description": "Certificate Files", + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "string" + }, + "certificate_key": { + "type": "string" + }, + "intermediate_certificate": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "certificate": "-----BEGIN CERTIFICATE-----\nMIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw\ngZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1\ncm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD\nDDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu\nb3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe\nbWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93\nQEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu\nGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+\n2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU\nQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB\nIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE\nOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G\nA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB\n/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy\nl8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s\nVXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn\nATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt\nIImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u\nm+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV\nXxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp\n1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw\nhp8bJUp/QN7pnOVCDbjTQ+HVMXw=\n-----END CERTIFICATE-----\n", + "certificate_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd\nqACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w\nrbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge\nYz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ\noxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z\nEo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X\nzGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU\nia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6\nYHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe\na0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu\nW0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw\no72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW\nH8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+\nN+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh\nELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU\nMDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31\nqjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq\ncMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9\nvMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO\nutTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V\ng0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1\nmJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq\nYatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8\nEQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk\n8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM\nRrKmPK/msHKK/sVHiL+NFqo=\n-----END PRIVATE KEY-----\n" + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "string", + "minLength": 1 + }, + "certificate_key": { + "type": "string", + "minLength": 1 + }, + "intermediate_certificate": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/get.json b/backend/schema/paths/nginx/certificates/get.json new file mode 100644 index 000000000..2f4b556af --- /dev/null +++ b/backend/schema/paths/nginx/certificates/get.json @@ -0,0 +1,54 @@ +{ + "operationId": "getCertificates", + "summary": "Get all certificates", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 4, + "created_on": "2024-10-09T05:31:58.000Z", + "modified_on": "2024-10-09T05:32:11.000Z", + "owner_user_id": 1, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "expires_on": "2025-01-07T04:34:18.000Z", + "meta": { + "letsencrypt_email": "jc@jc21.com", + "letsencrypt_agree": true, + "dns_challenge": false + } + } + ] + } + }, + "schema": { + "$ref": "../../../components/certificate-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/post.json b/backend/schema/paths/nginx/certificates/post.json new file mode 100644 index 000000000..5a3306c22 --- /dev/null +++ b/backend/schema/paths/nginx/certificates/post.json @@ -0,0 +1,97 @@ +{ + "operationId": "createCertificate", + "summary": "Create a Certificate", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "requestBody": { + "description": "Certificate Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["provider"], + "properties": { + "provider": { + "$ref": "../../../components/certificate-object.json#/properties/provider" + }, + "nice_name": { + "$ref": "../../../components/certificate-object.json#/properties/nice_name" + }, + "domain_names": { + "$ref": "../../../components/certificate-object.json#/properties/domain_names" + }, + "meta": { + "$ref": "../../../components/certificate-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires_on": "2025-01-07 04:30:17", + "modified_on": "2024-10-09 05:28:51", + "id": 5, + "created_on": "2024-10-09 05:28:35", + "owner_user_id": 1, + "is_deleted": false, + "provider": "letsencrypt", + "nice_name": "test.example.com", + "domain_names": ["test.example.com"], + "meta": { + "letsencrypt_email": "jc@jc21.com", + "letsencrypt_agree": true, + "dns_challenge": false, + "letsencrypt_certificate": { + "cn": "test.example.com", + "issuer": "C = US, O = Let's Encrypt, CN = E5", + "dates": { + "from": 1728448218, + "to": 1736224217 + } + } + } + } + } + }, + "schema": { + "$ref": "../../../components/certificate-object.json" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Domains are invalid" + } + } + } + }, + "schema": { + "$ref": "../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/test-http/get.json b/backend/schema/paths/nginx/certificates/test-http/get.json new file mode 100644 index 000000000..2b9a8dd3b --- /dev/null +++ b/backend/schema/paths/nginx/certificates/test-http/get.json @@ -0,0 +1,40 @@ +{ + "operationId": "testHttpReach", + "summary": "Test HTTP Reachability", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "parameters": [ + { + "in": "query", + "name": "domains", + "description": "Expansions", + "required": true, + "schema": { + "type": "string", + "example": "[\"test.example.ord\",\"test.example.com\",\"nonexistent.example.com\"]" + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "test.example.org": "ok", + "test.example.com": "other:Invalid domain or IP", + "nonexistent.example.com": "404" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/certificates/validate/post.json b/backend/schema/paths/nginx/certificates/validate/post.json new file mode 100644 index 000000000..21eb325ef --- /dev/null +++ b/backend/schema/paths/nginx/certificates/validate/post.json @@ -0,0 +1,114 @@ +{ + "operationId": "validateCertificates", + "summary": "Validates given Custom Certificates", + "tags": ["Certificates"], + "security": [ + { + "BearerAuth": ["certificates"] + } + ], + "requestBody": { + "description": "Certificate Files", + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "string" + }, + "certificate_key": { + "type": "string" + }, + "intermediate_certificate": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "certificate": { + "cn": "mkcert", + "issuer": "O = mkcert development CA, OU = jc@jc-Laptop.local (John Doe), CN = mkcert jc@jc-Laptop.local (John Doe)", + "dates": { + "from": 1728458537, + "to": 1799479337 + } + }, + "certificate_key": true + } + } + }, + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["certificate", "certificate_key"], + "properties": { + "certificate": { + "type": "object", + "additionalProperties": false, + "required": ["cn", "issuer", "dates"], + "properties": { + "cn": { + "type": "string" + }, + "issuer": { + "type": "string" + }, + "dates": { + "type": "object", + "additionalProperties": false, + "required": ["from", "to"], + "properties": { + "from": { + "type": "integer" + }, + "to": { + "type": "integer" + } + } + } + } + }, + "certificate_key": { + "type": "boolean" + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Certificate is not valid" + } + } + } + }, + "schema": { + "$ref": "../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/get.json b/backend/schema/paths/nginx/dead-hosts/get.json new file mode 100644 index 000000000..8a11a3f66 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/get.json @@ -0,0 +1,57 @@ +{ + "operationId": "getDeadHosts", + "summary": "Get all 404 hosts", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } + ] + } + }, + "schema": { + "$ref": "../../../components/dead-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/delete.json b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json new file mode 100644 index 000000000..f3aa81a5b --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteDeadHost", + "summary": "Delete a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json new file mode 100644 index 000000000..2cdcecf4b --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/disable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "disableDeadHost", + "summary": "Disable a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json new file mode 100644 index 000000000..ca3ce9fae --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/enable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "enableDeadHost", + "summary": "Enable a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/get.json b/backend/schema/paths/nginx/dead-hosts/hostID/get.json new file mode 100644 index 000000000..47e2f8b12 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/get.json @@ -0,0 +1,56 @@ +{ + "operationId": "getDeadHost", + "summary": "Get a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false + } + } + }, + "schema": { + "$ref": "../../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/hostID/put.json b/backend/schema/paths/nginx/dead-hosts/hostID/put.json new file mode 100644 index 000000000..f9505ed48 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/hostID/put.json @@ -0,0 +1,108 @@ +{ + "operationId": "updateDeadHost", + "summary": "Update a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "404 Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/dead-host-object.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../../../../components/dead-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/dead-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/dead-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/dead-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../../components/dead-host-object.json#/properties/http2_support" + }, + "advanced_config": { + "$ref": "../../../../components/dead-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../../components/dead-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:46:06.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate": null + } + } + }, + "schema": { + "$ref": "../../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/dead-hosts/post.json b/backend/schema/paths/nginx/dead-hosts/post.json new file mode 100644 index 000000000..c8bbb6932 --- /dev/null +++ b/backend/schema/paths/nginx/dead-hosts/post.json @@ -0,0 +1,93 @@ +{ + "operationId": "create404Host", + "summary": "Create a 404 Host", + "tags": ["404 Hosts"], + "security": [ + { + "BearerAuth": ["dead_hosts"] + } + ], + "requestBody": { + "description": "404 Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["domain_names"], + "properties": { + "domain_names": { + "$ref": "../../../components/dead-host-object.json#/properties/domain_names" + }, + "certificate_id": { + "$ref": "../../../components/dead-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/dead-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/dead-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/dead-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../components/dead-host-object.json#/properties/http2_support" + }, + "advanced_config": { + "$ref": "../../../components/dead-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../components/dead-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:38:52.000Z", + "modified_on": "2024-10-09T01:38:52.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "certificate_id": 0, + "ssl_forced": false, + "advanced_config": "", + "meta": {}, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + } + } + } + }, + "schema": { + "$ref": "../../../components/dead-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/get.json b/backend/schema/paths/nginx/proxy-hosts/get.json new file mode 100644 index 000000000..1d9f63351 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/get.json @@ -0,0 +1,65 @@ +{ + "operationId": "getProxyHosts", + "summary": "Get all proxy hosts", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["access_list", "owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-08T23:23:03.000Z", + "modified_on": "2024-10-08T23:23:04.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_host": "127.0.0.1", + "forward_port": 8989, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": null, + "hsts_enabled": false, + "hsts_subdomains": false + } + ] + } + }, + "schema": { + "$ref": "../../../components/proxy-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json new file mode 100644 index 000000000..991ef0e9e --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteProxyHost", + "summary": "Delete a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json new file mode 100644 index 000000000..54ff8a663 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/disable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "disableProxyHost", + "summary": "Disable a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json new file mode 100644 index 000000000..9f052de05 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/enable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "enableProxyHost", + "summary": "Enable a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/get.json b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json new file mode 100644 index 000000000..5e10a9cfd --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/get.json @@ -0,0 +1,64 @@ +{ + "operationId": "getProxyHost", + "summary": "Get a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T23:23:03.000Z", + "modified_on": "2024-10-08T23:26:38.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_host": "192.168.0.10", + "forward_port": 8989, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": null, + "hsts_enabled": false, + "hsts_subdomains": false + } + } + }, + "schema": { + "$ref": "../../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json new file mode 100644 index 000000000..5cab6e752 --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -0,0 +1,143 @@ +{ + "operationId": "updateProxyHost", + "summary": "Update a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T23:23:03.000Z", + "modified_on": "2024-10-08T23:26:37.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_host": "192.168.0.10", + "forward_port": 8989, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": ["admin"] + }, + "certificate": null, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json new file mode 100644 index 000000000..85455fb6b --- /dev/null +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -0,0 +1,128 @@ +{ + "operationId": "createProxyHost", + "summary": "Create a Proxy Host", + "tags": ["Proxy Hosts"], + "security": [ + { + "BearerAuth": ["proxy_hosts"] + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["domain_names", "forward_scheme", "forward_host", "forward_port"], + "properties": { + "domain_names": { + "$ref": "../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../components/proxy-host-object.json#/properties/locations" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-08T23:23:03.000Z", + "modified_on": "2024-10-08T23:23:03.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_host": "127.0.0.1", + "forward_port": 8989, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": {}, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2024-10-07T22:43:55.000Z", + "modified_on": "2024-10-08T12:52:54.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "some guy", + "avatar": "//www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?default=mm", + "roles": ["admin"] + }, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/get.json b/backend/schema/paths/nginx/redirection-hosts/get.json new file mode 100644 index 000000000..0b35e0fc4 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/get.json @@ -0,0 +1,62 @@ +{ + "operationId": "getRedirectionHosts", + "summary": "Get all Redirection hosts", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:13:13.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } + ] + } + }, + "schema": { + "$ref": "../../../components/redirection-host-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json new file mode 100644 index 000000000..7330f3623 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteRedirectionHost", + "summary": "Delete a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json new file mode 100644 index 000000000..8433220d5 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/disable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "disableRedirectionHost", + "summary": "Disable a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json new file mode 100644 index 000000000..bef53436b --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/enable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "enableRedirectionHost", + "summary": "Enable a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/get.json b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json new file mode 100644 index 000000000..d780f874b --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/get.json @@ -0,0 +1,61 @@ +{ + "operationId": "getRedirectionHost", + "summary": "Get a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:13:13.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301 + } + } + }, + "schema": { + "$ref": "../../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/hostID/put.json b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json new file mode 100644 index 000000000..fd97cbfa8 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/hostID/put.json @@ -0,0 +1,128 @@ +{ + "operationId": "updateRedirectionHost", + "summary": "Update a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Redirection Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/redirection-host-object.json#/properties/domain_names" + }, + "forward_http_code": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_http_code" + }, + "forward_scheme": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_scheme" + }, + "forward_domain_name": { + "$ref": "../../../../components/redirection-host-object.json#/properties/forward_domain_name" + }, + "preserve_path": { + "$ref": "../../../../components/redirection-host-object.json#/properties/preserve_path" + }, + "certificate_id": { + "$ref": "../../../../components/redirection-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/redirection-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/redirection-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/redirection-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../../components/redirection-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../../components/redirection-host-object.json#/properties/block_exploits" + }, + "advanced_config": { + "$ref": "../../../../components/redirection-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../../components/redirection-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:18:11.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate": null + } + } + }, + "schema": { + "$ref": "../../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/redirection-hosts/post.json b/backend/schema/paths/nginx/redirection-hosts/post.json new file mode 100644 index 000000000..5bfde2c38 --- /dev/null +++ b/backend/schema/paths/nginx/redirection-hosts/post.json @@ -0,0 +1,113 @@ +{ + "operationId": "createRedirectionHost", + "summary": "Create a Redirection Host", + "tags": ["Redirection Hosts"], + "security": [ + { + "BearerAuth": ["redirection_hosts"] + } + ], + "requestBody": { + "description": "Redirection Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["domain_names", "forward_scheme", "forward_http_code", "forward_domain_name"], + "properties": { + "domain_names": { + "$ref": "../../../components/redirection-host-object.json#/properties/domain_names" + }, + "forward_http_code": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_http_code" + }, + "forward_scheme": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_scheme" + }, + "forward_domain_name": { + "$ref": "../../../components/redirection-host-object.json#/properties/forward_domain_name" + }, + "preserve_path": { + "$ref": "../../../components/redirection-host-object.json#/properties/preserve_path" + }, + "certificate_id": { + "$ref": "../../../components/redirection-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/redirection-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/redirection-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/redirection-host-object.json#/properties/hsts_subdomains" + }, + "http2_support": { + "$ref": "../../../components/redirection-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../components/redirection-host-object.json#/properties/block_exploits" + }, + "advanced_config": { + "$ref": "../../../components/redirection-host-object.json#/properties/advanced_config" + }, + "meta": { + "$ref": "../../../components/redirection-host-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T01:13:12.000Z", + "modified_on": "2024-10-09T01:13:12.000Z", + "owner_user_id": 1, + "domain_names": ["test.example.com"], + "forward_domain_name": "something-else.com", + "preserve_path": false, + "certificate_id": 0, + "ssl_forced": false, + "block_exploits": false, + "advanced_config": "", + "meta": {}, + "http2_support": false, + "enabled": true, + "hsts_enabled": false, + "hsts_subdomains": false, + "forward_scheme": "http", + "forward_http_code": 301, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2024-10-09T00:59:56.000Z", + "modified_on": "2024-10-09T00:59:56.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + } + } + } + }, + "schema": { + "$ref": "../../../components/redirection-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/get.json b/backend/schema/paths/nginx/streams/get.json new file mode 100644 index 000000000..17969ee4e --- /dev/null +++ b/backend/schema/paths/nginx/streams/get.json @@ -0,0 +1,56 @@ +{ + "operationId": "getStreams", + "summary": "Get all streams", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "query", + "name": "expand", + "description": "Expansions", + "schema": { + "type": "string", + "enum": ["owner", "certificate"] + } + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": [ + { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "certificate_id": 0 + } + ] + } + }, + "schema": { + "$ref": "../../../components/stream-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/post.json b/backend/schema/paths/nginx/streams/post.json new file mode 100644 index 000000000..d26996b69 --- /dev/null +++ b/backend/schema/paths/nginx/streams/post.json @@ -0,0 +1,91 @@ +{ + "operationId": "createStream", + "summary": "Create a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "requestBody": { + "description": "Stream Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["incoming_port", "forwarding_host", "forwarding_port"], + "properties": { + "incoming_port": { + "$ref": "../../../components/stream-object.json#/properties/incoming_port" + }, + "forwarding_host": { + "$ref": "../../../components/stream-object.json#/properties/forwarding_host" + }, + "forwarding_port": { + "$ref": "../../../components/stream-object.json#/properties/forwarding_port" + }, + "tcp_forwarding": { + "$ref": "../../../components/stream-object.json#/properties/tcp_forwarding" + }, + "udp_forwarding": { + "$ref": "../../../components/stream-object.json#/properties/udp_forwarding" + }, + "certificate_id": { + "$ref": "../../../components/stream-object.json#/properties/certificate_id" + }, + "meta": { + "$ref": "../../../components/stream-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "owner": { + "id": 1, + "created_on": "2024-10-09T02:33:16.000Z", + "modified_on": "2024-10-09T02:33:16.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/delete.json b/backend/schema/paths/nginx/streams/streamID/delete.json new file mode 100644 index 000000000..3a9685258 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/delete.json @@ -0,0 +1,39 @@ +{ + "operationId": "deleteStream", + "summary": "Delete a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/disable/post.json b/backend/schema/paths/nginx/streams/streamID/disable/post.json new file mode 100644 index 000000000..d1c1b1c84 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/disable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "disableStream", + "summary": "Disable a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already disabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/enable/post.json b/backend/schema/paths/nginx/streams/streamID/enable/post.json new file mode 100644 index 000000000..dc914f5f2 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/enable/post.json @@ -0,0 +1,59 @@ +{ + "operationId": "enableStream", + "summary": "Enable a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "error": { + "code": 400, + "message": "Host is already enabled" + } + } + } + }, + "schema": { + "$ref": "../../../../../components/error.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/get.json b/backend/schema/paths/nginx/streams/streamID/get.json new file mode 100644 index 000000000..801af13a7 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/get.json @@ -0,0 +1,55 @@ +{ + "operationId": "getStream", + "summary": "Get a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/streams/streamID/put.json b/backend/schema/paths/nginx/streams/streamID/put.json new file mode 100644 index 000000000..14adb1631 --- /dev/null +++ b/backend/schema/paths/nginx/streams/streamID/put.json @@ -0,0 +1,103 @@ +{ + "operationId": "updateStream", + "summary": "Update a Stream", + "tags": ["Streams"], + "security": [ + { + "BearerAuth": ["streams"] + } + ], + "parameters": [ + { + "in": "path", + "name": "streamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Stream Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "incoming_port": { + "$ref": "../../../../components/stream-object.json#/properties/incoming_port" + }, + "forwarding_host": { + "$ref": "../../../../components/stream-object.json#/properties/forwarding_host" + }, + "forwarding_port": { + "$ref": "../../../../components/stream-object.json#/properties/forwarding_port" + }, + "tcp_forwarding": { + "$ref": "../../../../components/stream-object.json#/properties/tcp_forwarding" + }, + "udp_forwarding": { + "$ref": "../../../../components/stream-object.json#/properties/udp_forwarding" + }, + "certificate_id": { + "$ref": "../../../../components/stream-object.json#/properties/certificate_id" + }, + "meta": { + "$ref": "../../../../components/stream-object.json#/properties/meta" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 1, + "created_on": "2024-10-09T02:33:45.000Z", + "modified_on": "2024-10-09T02:33:45.000Z", + "owner_user_id": 1, + "incoming_port": 9090, + "forwarding_host": "router.internal", + "forwarding_port": 80, + "tcp_forwarding": true, + "udp_forwarding": false, + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "enabled": true, + "owner": { + "id": 1, + "created_on": "2024-10-09T02:33:16.000Z", + "modified_on": "2024-10-09T02:33:16.000Z", + "is_deleted": false, + "is_disabled": false, + "email": "admin@example.com", + "name": "Administrator", + "nickname": "Admin", + "avatar": "", + "roles": ["admin"] + }, + "certificate_id": 0 + } + } + }, + "schema": { + "$ref": "../../../../components/stream-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/reports/hosts/get.json b/backend/schema/paths/reports/hosts/get.json new file mode 100644 index 000000000..a40ddc723 --- /dev/null +++ b/backend/schema/paths/reports/hosts/get.json @@ -0,0 +1,50 @@ +{ + "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": { + "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" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/schema/get.json b/backend/schema/paths/schema/get.json new file mode 100644 index 000000000..d435b0042 --- /dev/null +++ b/backend/schema/paths/schema/get.json @@ -0,0 +1,10 @@ +{ + "operationId": "schema", + "summary": "Returns this swagger API schema", + "tags": ["Public"], + "responses": { + "200": { + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/settings/get.json b/backend/schema/paths/settings/get.json new file mode 100644 index 000000000..5d148d8af --- /dev/null +++ b/backend/schema/paths/settings/get.json @@ -0,0 +1,35 @@ +{ + "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/setting-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/settings/settingID/get.json b/backend/schema/paths/settings/settingID/get.json new file mode 100644 index 000000000..405b976d2 --- /dev/null +++ b/backend/schema/paths/settings/settingID/get.json @@ -0,0 +1,46 @@ +{ + "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/setting-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/settings/settingID/put.json b/backend/schema/paths/settings/settingID/put.json new file mode 100644 index 000000000..4ca624293 --- /dev/null +++ b/backend/schema/paths/settings/settingID/put.json @@ -0,0 +1,79 @@ +{ + "operationId": "updateSetting", + "summary": "Update a setting", + "tags": ["Settings"], + "security": [ + { + "BearerAuth": ["settings"] + } + ], + "parameters": [ + { + "in": "path", + "name": "settingID", + "schema": { + "type": "string", + "minLength": 1, + "enum": ["default-site"] + }, + "required": true, + "description": "Setting ID", + "example": "default-site" + } + ], + "requestBody": { + "description": "Setting Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "value": { + "type": "string", + "minLength": 1, + "enum": ["congratulations", "404", "444", "redirect", "html"] + }, + "meta": { + "type": "object", + "additionalProperties": false, + "properties": { + "redirect": { + "type": "string" + }, + "html": { + "type": "string" + } + } + } + } + } + } + } + }, + "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/setting-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/tokens/get.json b/backend/schema/paths/tokens/get.json new file mode 100644 index 000000000..ef842eafe --- /dev/null +++ b/backend/schema/paths/tokens/get.json @@ -0,0 +1,30 @@ +{ + "operationId": "refreshToken", + "summary": "Refresh your access token", + "tags": ["Tokens"], + "security": [ + { + "BearerAuth": ["tokens"] + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "expires": "2025-02-04T20:40:46.340Z", + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } + }, + "schema": { + "$ref": "../../components/token-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/tokens/post.json b/backend/schema/paths/tokens/post.json new file mode 100644 index 000000000..99703ff0d --- /dev/null +++ b/backend/schema/paths/tokens/post.json @@ -0,0 +1,55 @@ +{ + "operationId": "requestToken", + "summary": "Request a new access token from credentials", + "tags": ["Tokens"], + "requestBody": { + "description": "Credentials Payload", + "required": true, + "content": { + "application/json": { + "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": "2025-02-04T20:40:46.340Z", + "token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4" + } + } + } + }, + "schema": { + "$ref": "../../components/token-object.json" + } + } + }, + "description": "200 response" + } + } +} diff --git a/backend/schema/paths/users/get.json b/backend/schema/paths/users/get.json new file mode 100644 index 000000000..374153011 --- /dev/null +++ b/backend/schema/paths/users/get.json @@ -0,0 +1,74 @@ +{ + "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": false, + "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": false, + "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/user-list.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/post.json b/backend/schema/paths/users/post.json new file mode 100644 index 000000000..c0213fe05 --- /dev/null +++ b/backend/schema/paths/users/post.json @@ -0,0 +1,88 @@ +{ + "operationId": "createUser", + "summary": "Create a User", + "tags": ["Users"], + "security": [ + { + "BearerAuth": ["users"] + } + ], + "requestBody": { + "description": "User Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "nickname", "email"], + "properties": { + "name": { + "$ref": "../../components/user-object.json#/properties/name" + }, + "nickname": { + "$ref": "../../components/user-object.json#/properties/nickname" + }, + "email": { + "$ref": "../../components/user-object.json#/properties/email" + }, + "roles": { + "$ref": "../../components/user-object.json#/properties/roles" + }, + "is_disabled": { + "$ref": "../../components/user-object.json#/properties/is_disabled" + }, + "auth": { + "type": "object", + "description": "Auth Credentials", + "example": { + "type": "password", + "secret": "bigredhorsebanana" + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 2, + "created_on": "2020-01-30T09:41:04.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"], + "permissions": { + "id": 3, + "created_on": "2020-01-30T09:41:04.000Z", + "modified_on": "2020-01-30T09:41:04.000Z", + "user_id": 2, + "visibility": "user", + "proxy_hosts": "manage", + "redirection_hosts": "manage", + "dead_hosts": "manage", + "streams": "manage", + "access_lists": "manage", + "certificates": "manage" + } + } + } + }, + "schema": { + "$ref": "../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/auth/put.json b/backend/schema/paths/users/userID/auth/put.json new file mode 100644 index 000000000..a72f5617c --- /dev/null +++ b/backend/schema/paths/users/userID/auth/put.json @@ -0,0 +1,79 @@ +{ + "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 + } + ], + "requestBody": { + "description": "Auth Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "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!" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/delete.json b/backend/schema/paths/users/userID/delete.json new file mode 100644 index 000000000..7d4f36151 --- /dev/null +++ b/backend/schema/paths/users/userID/delete.json @@ -0,0 +1,40 @@ +{ + "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" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/get.json b/backend/schema/paths/users/userID/get.json new file mode 100644 index 000000000..cb8ac61b4 --- /dev/null +++ b/backend/schema/paths/users/userID/get.json @@ -0,0 +1,58 @@ +{ + "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": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + } + }, + "schema": { + "$ref": "../../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/login/post.json b/backend/schema/paths/users/userID/login/post.json new file mode 100644 index 000000000..6148d182b --- /dev/null +++ b/backend/schema/paths/users/userID/login/post.json @@ -0,0 +1,73 @@ +{ + "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": false, + "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/user-object.json" + } + } + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/permissions/put.json b/backend/schema/paths/users/userID/permissions/put.json new file mode 100644 index 000000000..2dcd2aed7 --- /dev/null +++ b/backend/schema/paths/users/userID/permissions/put.json @@ -0,0 +1,51 @@ +{ + "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 + } + ], + "requestBody": { + "description": "Permissions Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "../../../../components/permission-object.json" + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": true + } + }, + "schema": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/schema/paths/users/userID/put.json b/backend/schema/paths/users/userID/put.json new file mode 100644 index 000000000..60a6cd132 --- /dev/null +++ b/backend/schema/paths/users/userID/put.json @@ -0,0 +1,88 @@ +{ + "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 + } + ], + "requestBody": { + "description": "User Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "name": { + "$ref": "../../../components/user-object.json#/properties/name" + }, + "nickname": { + "$ref": "../../../components/user-object.json#/properties/nickname" + }, + "email": { + "$ref": "../../../components/user-object.json#/properties/email" + }, + "roles": { + "$ref": "../../../components/user-object.json#/properties/roles" + }, + "is_disabled": { + "$ref": "../../../components/user-object.json#/properties/is_disabled" + } + } + } + } + } + }, + "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": false, + "email": "jc@jc21.com", + "name": "Jamie Curnow", + "nickname": "James", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": ["admin"] + } + } + }, + "schema": { + "$ref": "../../../components/user-object.json" + } + } + } + } + } +} diff --git a/backend/schema/swagger.json b/backend/schema/swagger.json new file mode 100644 index 000000000..4a502b4e4 --- /dev/null +++ b/backend/schema/swagger.json @@ -0,0 +1,274 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Nginx Proxy Manager API", + "version": "2.x.x" + }, + "servers": [ + { + "url": "http://127.0.0.1:81/api" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + }, + "paths": { + "/": { + "get": { + "$ref": "./paths/get.json" + } + }, + "/audit-log": { + "get": { + "$ref": "./paths/audit-log/get.json" + } + }, + "/nginx/access-lists": { + "get": { + "$ref": "./paths/nginx/access-lists/get.json" + }, + "post": { + "$ref": "./paths/nginx/access-lists/post.json" + } + }, + "/nginx/access-lists/{listID}": { + "get": { + "$ref": "./paths/nginx/access-lists/listID/get.json" + }, + "put": { + "$ref": "./paths/nginx/access-lists/listID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/access-lists/listID/delete.json" + } + }, + "/nginx/certificates": { + "get": { + "$ref": "./paths/nginx/certificates/get.json" + }, + "post": { + "$ref": "./paths/nginx/certificates/post.json" + } + }, + "/nginx/certificates/validate": { + "post": { + "$ref": "./paths/nginx/certificates/validate/post.json" + } + }, + "/nginx/certificates/test-http": { + "get": { + "$ref": "./paths/nginx/certificates/test-http/get.json" + } + }, + "/nginx/certificates/{certID}": { + "get": { + "$ref": "./paths/nginx/certificates/certID/get.json" + }, + "delete": { + "$ref": "./paths/nginx/certificates/certID/delete.json" + } + }, + "/nginx/certificates/{certID}/download": { + "get": { + "$ref": "./paths/nginx/certificates/certID/download/get.json" + } + }, + "/nginx/certificates/{certID}/renew": { + "post": { + "$ref": "./paths/nginx/certificates/certID/renew/post.json" + } + }, + "/nginx/certificates/{certID}/upload": { + "post": { + "$ref": "./paths/nginx/certificates/certID/upload/post.json" + } + }, + "/nginx/proxy-hosts": { + "get": { + "$ref": "./paths/nginx/proxy-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/proxy-hosts/post.json" + } + }, + "/nginx/proxy-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/proxy-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/proxy-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/proxy-hosts/hostID/delete.json" + } + }, + "/nginx/proxy-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/proxy-hosts/hostID/enable/post.json" + } + }, + "/nginx/proxy-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/proxy-hosts/hostID/disable/post.json" + } + }, + "/nginx/redirection-hosts": { + "get": { + "$ref": "./paths/nginx/redirection-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/redirection-hosts/post.json" + } + }, + "/nginx/redirection-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/redirection-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/redirection-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/redirection-hosts/hostID/delete.json" + } + }, + "/nginx/redirection-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/redirection-hosts/hostID/enable/post.json" + } + }, + "/nginx/redirection-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/redirection-hosts/hostID/disable/post.json" + } + }, + "/nginx/dead-hosts": { + "get": { + "$ref": "./paths/nginx/dead-hosts/get.json" + }, + "post": { + "$ref": "./paths/nginx/dead-hosts/post.json" + } + }, + "/nginx/dead-hosts/{hostID}": { + "get": { + "$ref": "./paths/nginx/dead-hosts/hostID/get.json" + }, + "put": { + "$ref": "./paths/nginx/dead-hosts/hostID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/dead-hosts/hostID/delete.json" + } + }, + "/nginx/dead-hosts/{hostID}/enable": { + "post": { + "$ref": "./paths/nginx/dead-hosts/hostID/enable/post.json" + } + }, + "/nginx/dead-hosts/{hostID}/disable": { + "post": { + "$ref": "./paths/nginx/dead-hosts/hostID/disable/post.json" + } + }, + "/nginx/streams": { + "get": { + "$ref": "./paths/nginx/streams/get.json" + }, + "post": { + "$ref": "./paths/nginx/streams/post.json" + } + }, + "/nginx/streams/{streamID}": { + "get": { + "$ref": "./paths/nginx/streams/streamID/get.json" + }, + "put": { + "$ref": "./paths/nginx/streams/streamID/put.json" + }, + "delete": { + "$ref": "./paths/nginx/streams/streamID/delete.json" + } + }, + "/nginx/streams/{streamID}/enable": { + "post": { + "$ref": "./paths/nginx/streams/streamID/enable/post.json" + } + }, + "/nginx/streams/{streamID}/disable": { + "post": { + "$ref": "./paths/nginx/streams/streamID/disable/post.json" + } + }, + "/reports/hosts": { + "get": { + "$ref": "./paths/reports/hosts/get.json" + } + }, + "/schema": { + "get": { + "$ref": "./paths/schema/get.json" + } + }, + "/settings": { + "get": { + "$ref": "./paths/settings/get.json" + } + }, + "/settings/{settingID}": { + "get": { + "$ref": "./paths/settings/settingID/get.json" + }, + "put": { + "$ref": "./paths/settings/settingID/put.json" + } + }, + "/tokens": { + "get": { + "$ref": "./paths/tokens/get.json" + }, + "post": { + "$ref": "./paths/tokens/post.json" + } + }, + "/users": { + "get": { + "$ref": "./paths/users/get.json" + }, + "post": { + "$ref": "./paths/users/post.json" + } + }, + "/users/{userID}": { + "get": { + "$ref": "./paths/users/userID/get.json" + }, + "put": { + "$ref": "./paths/users/userID/put.json" + }, + "delete": { + "$ref": "./paths/users/userID/delete.json" + } + }, + "/users/{userID}/auth": { + "put": { + "$ref": "./paths/users/userID/auth/put.json" + } + }, + "/users/{userID}/permissions": { + "put": { + "$ref": "./paths/users/userID/permissions/put.json" + } + }, + "/users/{userID}/login": { + "post": { + "$ref": "./paths/users/userID/login/post.json" + } + } + } +} diff --git a/backend/scripts/install-certbot-plugins b/backend/scripts/install-certbot-plugins new file mode 100755 index 000000000..bf9954105 --- /dev/null +++ b/backend/scripts/install-certbot-plugins @@ -0,0 +1,49 @@ +#!/usr/bin/node + +// Usage: +// Install all plugins defined in `certbot-dns-plugins.json`: +// ./install-certbot-plugins +// Install one or more specific plugins: +// ./install-certbot-plugins route53 cloudflare +// +// Usage with a running docker container: +// docker exec npm_core /command/s6-setuidgid 1000:1000 bash -c "/app/scripts/install-certbot-plugins" +// + +const dnsPlugins = require('../global/certbot-dns-plugins.json'); +const certbot = require('../lib/certbot'); +const logger = require('../logger').certbot; +const batchflow = require('batchflow'); + +let hasErrors = false; +let failingPlugins = []; + +let pluginKeys = Object.keys(dnsPlugins); +if (process.argv.length > 2) { + pluginKeys = process.argv.slice(2); +} + +batchflow(pluginKeys).sequential() + .each((i, pluginKey, next) => { + certbot.installPlugin(pluginKey) + .then(() => { + next(); + }) + .catch((err) => { + hasErrors = true; + failingPlugins.push(pluginKey); + next(err); + }); + }) + .error((err) => { + logger.error(err.message); + }) + .end(() => { + if (hasErrors) { + logger.error('Some plugins failed to install. Please check the logs above. Failing plugins: ' + '\n - ' + failingPlugins.join('\n - ')); + process.exit(1); + } else { + logger.complete('Plugins installed successfully'); + process.exit(0); + } + }); diff --git a/backend/setup.js b/backend/setup.js new file mode 100644 index 000000000..6b9b8e78a --- /dev/null +++ b/backend/setup.js @@ -0,0 +1,173 @@ +const config = require('./lib/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 certbot = require('./lib/certbot'); +/** + * Creates a default admin users if one doesn't already exist in the database + * + * @returns {Promise} + */ +const setupDefaultUser = () => { + return userModel + .query() + .select('id', ) + .where('is_deleted', 0) + .first() + .then((row) => { + if (!row || !row.id) { + // Create a new user and set password + const email = process.env.INITIAL_ADMIN_EMAIL || 'admin@example.com'; + const password = process.env.INITIAL_ADMIN_PASSWORD || 'changeme'; + + logger.info('Creating a new user: ' + email + ' with password: ' + password); + + const data = { + is_deleted: 0, + email: email, + 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: password, + 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 (config.debug()) { + logger.info('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('id') + .where({id: 'default-site'}) + .first() + .then((row) => { + if (!row || !row.id) { + 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 (config.debug()) { + logger.info('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) { + if (plugins.indexOf(certificate.meta.dns_provider) === -1) { + plugins.push(certificate.meta.dns_provider); + } + + // 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)); + } + }); + + return certbot.installPlugins(plugins) + .then(() => { + 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 setupDefaultUser() + .then(setupDefaultSettings) + .then(setupCertbotPlugins) + .then(setupLogrotation); +}; diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf new file mode 100644 index 000000000..f59263775 --- /dev/null +++ b/backend/templates/_access.conf @@ -0,0 +1,25 @@ +{% 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 }}; + + {% if access_list.pass_auth == 0 or access_list.pass_auth == true %} + proxy_set_header Authorization ""; + {% endif %} + + {% endif %} + + # Access Rules: {{ access_list.clients | size }} total + {% for client in access_list.clients %} + {{client | nginxAccessRule}} + {% endfor %} + deny all; + + # Access checks must... + {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %} + satisfy any; + {% else %} + satisfy all; + {% endif %} +{% endif %} diff --git a/src/backend/templates/_assets.conf b/backend/templates/_assets.conf similarity index 100% rename from src/backend/templates/_assets.conf rename to backend/templates/_assets.conf diff --git a/src/backend/templates/_certificates.conf b/backend/templates/_certificates.conf similarity index 93% rename from src/backend/templates/_certificates.conf rename to backend/templates/_certificates.conf index 06ca7bb87..efcca5cd5 100644 --- a/src/backend/templates/_certificates.conf +++ b/backend/templates/_certificates.conf @@ -2,6 +2,7 @@ {% if certificate.provider == "letsencrypt" %} # Let's Encrypt SSL include conf.d/include/letsencrypt-acme-challenge.conf; + include conf.d/include/ssl-cache.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; diff --git a/backend/templates/_certificates_stream.conf b/backend/templates/_certificates_stream.conf new file mode 100644 index 000000000..ba7812fdd --- /dev/null +++ b/backend/templates/_certificates_stream.conf @@ -0,0 +1,13 @@ +{% if certificate and certificate_id > 0 %} +{% if certificate.provider == "letsencrypt" %} + # Let's Encrypt SSL + include conf.d/include/ssl-cache-stream.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/src/backend/templates/_exploits.conf b/backend/templates/_exploits.conf similarity index 100% rename from src/backend/templates/_exploits.conf rename to backend/templates/_exploits.conf diff --git a/src/backend/templates/_forced_ssl.conf b/backend/templates/_forced_ssl.conf similarity index 100% rename from src/backend/templates/_forced_ssl.conf rename to backend/templates/_forced_ssl.conf diff --git a/src/backend/templates/_header_comment.conf b/backend/templates/_header_comment.conf similarity index 100% rename from src/backend/templates/_header_comment.conf rename to backend/templates/_header_comment.conf diff --git a/backend/templates/_hsts.conf b/backend/templates/_hsts.conf new file mode 100644 index 000000000..26c83ee8b --- /dev/null +++ b/backend/templates/_hsts.conf @@ -0,0 +1,8 @@ +{% 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 $hsts_header always; +{% endif %} +{% endif %} +{% endif %} diff --git a/backend/templates/_hsts_map.conf b/backend/templates/_hsts_map.conf new file mode 100644 index 000000000..27dd1f8f4 --- /dev/null +++ b/backend/templates/_hsts_map.conf @@ -0,0 +1,3 @@ +map $scheme $hsts_header { + https "max-age=63072000;{% if hsts_subdomains == 1 or hsts_subdomains == true -%} includeSubDomains;{% endif %} preload"; +} \ No newline at end of file diff --git a/backend/templates/_listen.conf b/backend/templates/_listen.conf new file mode 100644 index 000000000..34a808e6a --- /dev/null +++ b/backend/templates/_listen.conf @@ -0,0 +1,20 @@ + listen 80; +{% if ipv6 -%} + listen [::]:80; +{% else -%} + #listen [::]:80; +{% endif %} +{% if certificate -%} + listen 443 ssl; +{% if ipv6 -%} + listen [::]:443 ssl; +{% else -%} + #listen [::]:443; +{% endif %} +{% endif %} + server_name {{ domain_names | join: " " }}; +{% if http2_support == 1 or http2_support == true %} + http2 on; +{% else -%} + http2 off; +{% endif %} \ No newline at end of file diff --git a/backend/templates/_location.conf b/backend/templates/_location.conf new file mode 100644 index 000000000..a2ecb166d --- /dev/null +++ b/backend/templates/_location.conf @@ -0,0 +1,24 @@ + location {{ path }} { + {{ advanced_config }} + + 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 }}; + + {% include "_access.conf" %} + {% 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 %} + } + diff --git a/src/backend/templates/dead_host.conf b/backend/templates/dead_host.conf similarity index 60% rename from src/backend/templates/dead_host.conf rename to backend/templates/dead_host.conf index da282a12b..2e7d2a007 100644 --- a/src/backend/templates/dead_host.conf +++ b/backend/templates/dead_host.conf @@ -1,22 +1,28 @@ {% include "_header_comment.conf" %} {% if enabled %} + +{% include "_hsts_map.conf" %} + server { {% include "_listen.conf" %} {% include "_certificates.conf" %} {% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} - access_log /data/logs/dead_host-{{ id }}.log standard; + 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 "_forced_ssl.conf" %} {% include "_hsts.conf" %} return 404; } {% endif %} + # Custom + include /data/nginx/custom/server_dead[.]conf; } {% endif %} diff --git a/src/backend/templates/default.conf b/backend/templates/default.conf similarity index 66% rename from src/backend/templates/default.conf rename to backend/templates/default.conf index 7ed1af978..cc590f9d8 100644 --- a/src/backend/templates/default.conf +++ b/backend/templates/default.conf @@ -6,16 +6,30 @@ {%- 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.log combined; + 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 == "444" %} + location / { + return 444; + } +{% endif %} + {%- if value == "redirect" %} location / { return 301 {{ meta.redirect }}; diff --git a/src/backend/templates/ip_ranges.conf b/backend/templates/ip_ranges.conf similarity index 100% rename from src/backend/templates/ip_ranges.conf rename to backend/templates/ip_ranges.conf diff --git a/src/backend/templates/letsencrypt-request.conf b/backend/templates/letsencrypt-request.conf similarity index 54% rename from src/backend/templates/letsencrypt-request.conf rename to backend/templates/letsencrypt-request.conf index 2adcdb328..676c8a60f 100644 --- a/src/backend/templates/letsencrypt-request.conf +++ b/backend/templates/letsencrypt-request.conf @@ -2,9 +2,14 @@ server { listen 80; +{% if ipv6 -%} + listen [::]:80; +{% endif %} + server_name {{ domain_names | join: " " }}; - access_log /data/logs/letsencrypt-requests.log standard; + 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; diff --git a/src/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf similarity index 65% rename from src/backend/templates/proxy_host.conf rename to backend/templates/proxy_host.conf index 8d8c97da8..d23ca46fa 100644 --- a/src/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -1,6 +1,9 @@ {% include "_header_comment.conf" %} {% if enabled %} + +{% include "_hsts_map.conf" %} + server { set $forward_scheme {{ forward_scheme }}; set $server "{{ forward_host }}"; @@ -11,8 +14,16 @@ server { {% include "_assets.conf" %} {% include "_exploits.conf" %} {% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} - access_log /data/logs/proxy_host-{{ id }}.log proxy; +{% 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 }} @@ -21,18 +32,13 @@ server { {% if use_default_location %} location / { - {%- if access_list_id > 0 -%} - # Access List - auth_basic "Authorization required"; - auth_basic_user_file /data/access/{{ access_list_id }}; - {%- endif %} -{% include "_forced_ssl.conf" %} +{% include "_access.conf" %} {% include "_hsts.conf" %} {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %} proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + proxy_set_header Connection $http_connection; proxy_http_version 1.1; {% endif %} diff --git a/src/backend/templates/redirection_host.conf b/backend/templates/redirection_host.conf similarity index 59% rename from src/backend/templates/redirection_host.conf rename to backend/templates/redirection_host.conf index 463f3a8eb..7dd360795 100644 --- a/src/backend/templates/redirection_host.conf +++ b/backend/templates/redirection_host.conf @@ -1,26 +1,30 @@ {% include "_header_comment.conf" %} {% if enabled %} + +{% include "_hsts_map.conf" %} + server { {% include "_listen.conf" %} {% include "_certificates.conf" %} {% include "_assets.conf" %} {% include "_exploits.conf" %} {% include "_hsts.conf" %} +{% include "_forced_ssl.conf" %} - access_log /data/logs/redirection_host-{{ id }}.log standard; + 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 "_forced_ssl.conf" %} {% include "_hsts.conf" %} {% if preserve_path == 1 or preserve_path == true %} - return 301 $scheme://{{ forward_domain_name }}$request_uri; + return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}$request_uri; {% else %} - return 301 $scheme://{{ forward_domain_name }}; + return {{ forward_http_code }} {{ forward_scheme }}://{{ forward_domain_name }}; {% endif %} } {% endif %} diff --git a/src/backend/templates/stream.conf b/backend/templates/stream.conf similarity index 55% rename from src/backend/templates/stream.conf rename to backend/templates/stream.conf index a6dc054f8..7333aaee1 100644 --- a/src/backend/templates/stream.conf +++ b/backend/templates/stream.conf @@ -5,18 +5,25 @@ {% if enabled %} {% if tcp_forwarding == 1 or tcp_forwarding == true -%} server { - listen {{ incoming_port }}; - proxy_pass {{ forward_ip }}:{{ forwarding_port }}; + listen {{ incoming_port }} {%- if certificate %} ssl {%- endif %}; + {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} {%- if certificate %} ssl {%- endif %}; + + {%- include "_certificates_stream.conf" %} + + 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 %} + +{% if udp_forwarding == 1 or udp_forwarding == true -%} server { listen {{ incoming_port }} udp; - proxy_pass {{ forward_ip }}:{{ forwarding_port }}; + {% unless ipv6 -%} # {%- endunless -%} listen [::]:{{ incoming_port }} udp; + + proxy_pass {{ forwarding_host }}:{{ forwarding_port }}; # Custom include /data/nginx/custom/server_stream[.]conf; diff --git a/backend/validate-schema.js b/backend/validate-schema.js new file mode 100644 index 000000000..71a05c818 --- /dev/null +++ b/backend/validate-schema.js @@ -0,0 +1,16 @@ +const SwaggerParser = require('@apidevtools/swagger-parser'); +const chalk = require('chalk'); +const schema = require('./schema'); +const log = console.log; + +schema.getCompiledSchema().then(async (swaggerJSON) => { + try { + const api = await SwaggerParser.validate(swaggerJSON); + console.log('API name: %s, Version: %s', api.info.title, api.info.version); + log(chalk.green('❯ Schema is valid')); + } catch (e) { + console.error(e); + log(chalk.red('❯', e.message), '\n'); + process.exit(1); + } +}); diff --git a/backend/yarn.lock b/backend/yarn.lock new file mode 100644 index 000000000..bae734b43 --- /dev/null +++ b/backend/yarn.lock @@ -0,0 +1,3808 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@apidevtools/json-schema-ref-parser@9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" + integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + js-yaml "^3.13.1" + +"@apidevtools/json-schema-ref-parser@^11.7.0": + version "11.7.0" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.0.tgz#228d72018a0e7cbee744b677eaa01a8968f302d9" + integrity sha512-pRrmXMCwnmrkS3MLgAIW5dXRzeTv6GLjkjb4HmxNnvAKXN1Nfzp4KmGADBQvlVUcqi+a5D+hfGDLLnd5NnYxog== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.15" + js-yaml "^4.1.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== + +"@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: + "@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" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.3.0.tgz#a556790523a351b4e47e9d385f47265eaaf9780a" + integrity sha512-v3oplH6FYCULtFuCeqyuTd9D2WKO937Dxdq+GmHOLL72TTRriLxz2VLlNfkZRsvj6PKnOPAtuT6dwrs/pA5DvA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403" + integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ== + +"@eslint/eslintrc@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.1.tgz#7888fe7ec8f21bc26d646dbd2c11cd776e21192d" + integrity sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.5.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.36.0": + version "8.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe" + integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg== + +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@humanwhocodes/config-array@^0.11.8": + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.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== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.10" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" + integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@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: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5": + 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.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: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@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" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@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/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== + +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.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +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.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" + integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== + dependencies: + debug "^4.1.0" + depd "^2.0.0" + humanize-ms "^1.2.1" + +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.10.0, 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 "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.17.1, ajv@^8.6.3: + 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.2: + 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" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + 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-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-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.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== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +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@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.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" + +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== + +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= + +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" + +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +aws-ssl-profiles@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" + integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== + +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== + +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" + +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.20.3, body-parser@^1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.13.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +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.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.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.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +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-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" + 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@4.1.2, chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.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" + +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== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +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== + +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-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== + +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= + +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== + +color-support@^1.1.2, color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1" + integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA== + +commander@^9.1.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +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= + +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, 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.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +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.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +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@^7.0.2: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +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: + 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, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: + 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@^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= + +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.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-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: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.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= + +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + +depd@2.0.0, depd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +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= + +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + +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= + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +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" + +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== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +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" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + 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-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= + +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-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@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.36.0: + version "8.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.36.0.tgz#1bd72202200a5492f91803b113fb8a83b11285cf" + integrity sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.4.0" + "@eslint/eslintrc" "^2.0.1" + "@eslint/js" "8.36.0" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-visitor-keys "^3.3.0" + espree "^9.5.0" + esquery "^1.4.2" + esutils "^2.0.2" + 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" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +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@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.0.tgz#3646d4e3f58907464edba852fa047e6a27bdf113" + integrity sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.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.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.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 "^5.2.0" + +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== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +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== + +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.20.0: + version "4.20.0" + resolved "https://registry.yarnpkg.com/express/-/express-4.20.0.tgz#f1d08e591fcec770c07be4767af8eb9bcfd67c48" + integrity sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.3" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.10" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.19.0" + serve-static "1.16.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +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-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 sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-uri@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" + integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +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" + +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: + flat-cache "^3.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + 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" + +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 "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +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-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== + dependencies: + minipass "^3.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@~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== + +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== + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +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" + +generate-function@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +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-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-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +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" + +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== + +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.0: + 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.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" + +globals@^13.19.0: + version "13.20.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" + integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== + dependencies: + type-fest "^0.20.2" + +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: + get-intrinsic "^1.1.3" + +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== + +graceful-fs@^4.2.6: + 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== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +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-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-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +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== + +has@^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" + +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.2" + +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +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" + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +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@^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.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-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= + +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== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +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: + 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.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== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ip@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.1.tgz#e8f3595d33a3ea66490204234b77636965307105" + integrity sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ== + +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-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-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.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +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-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: + 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-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + +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@^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-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-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g== + +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-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: + 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= + +js-sdsl@^4.1.4: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + +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" + +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-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-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= + +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +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" + +knex@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/knex/-/knex-2.4.2.tgz#a34a289d38406dc19a0447a78eeaf2d16ebedd61" + integrity sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg== + dependencies: + colorette "2.0.19" + commander "^9.1.0" + debug "4.3.4" + escalade "^3.1.1" + esm "^3.2.25" + get-package-type "^0.1.0" + getopts "2.3.0" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.5.0" + rechoir "^0.8.0" + resolve-from "^5.0.0" + tarn "^3.0.2" + tildify "2.0.0" + +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.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.2.1" + type-check "~0.4.0" + +liquidjs@10.6.1: + version "10.6.1" + resolved "https://registry.yarnpkg.com/liquidjs/-/liquidjs-10.6.1.tgz#b401662cb8f0cca59b42f79fc08e411c86d92dab" + integrity sha512-6yUDD8i6QRgVppB8dD73Z672lNa2pxHMsMNEZvbVQyj937wMk/kbKfhSnN9Sess/k8eRdgKeQHsTGIiCGkWrGw== + dependencies: + commander "^10.0.0" + +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" + +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 "^5.0.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.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.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.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.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +long@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + +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== + +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== + dependencies: + yallist "^4.0.0" + +lru-cache@^7.14.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +lru-cache@^8.0.0: + version "8.0.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" + integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== + +make-dir@^3.0.0, make-dir@^3.1.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-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.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.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +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-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +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-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.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-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" + +minimatch@^3.0.5, 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@^1.2.0, minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +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" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.0.0: + version "4.2.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.5.tgz#9e0e5256f1e3513f8c34691dd68549e85b2c8ceb" + integrity sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q== + +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" + +minizlib@^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== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +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" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + +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.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, ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mysql2@^3.11.1: + version "3.11.1" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.11.1.tgz#edfb856e2176fcf43d2cc066dd4959e9fc76ea85" + integrity sha512-Oc8Zffd0gpIJnJ/NOMp6IiiJJDdWc7nmWpS+UE3K9feTpYia8XkbgL6EaOJYz52f6+2pAoC0eAQqUzal4lnNGQ== + dependencies: + aws-ssl-profiles "^1.1.1" + denque "^2.1.0" + generate-function "^2.3.1" + iconv-lite "^0.6.3" + long "^5.2.1" + lru-cache "^8.0.0" + named-placeholders "^1.1.3" + seq-queue "^0.0.5" + sqlstring "^2.3.2" + +named-placeholders@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" + integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== + dependencies: + lru-cache "^7.14.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.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.3, negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +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-addon-api@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + +node-fetch@^2.6.7: + version "2.6.9" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" + integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== + dependencies: + whatwg-url "^5.0.0" + +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +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-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@^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-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" + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.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, 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-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== + +objection@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/objection/-/objection-3.0.1.tgz#f67dc698187d10524e5d1b5d37a54e5bba49a42a" + integrity sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q== + dependencies: + ajv "^8.6.2" + db-errors "^0.2.3" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + 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" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + 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" + +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: + 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-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: + yocto-queue "^0.1.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-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 "^3.0.2" + +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: + aggregate-error "^3.0.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-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" + +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== + +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.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.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-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== + +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-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" + integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== + +pg-connection-string@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" + integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" + integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== + +pg-protocol@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" + integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.13.1: + version "8.13.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080" + integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ== + dependencies: + pg-connection-string "^2.7.0" + pg-pool "^3.7.0" + pg-protocol "^1.7.0" + pg-types "^2.1.0" + pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pkg-conf@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" + integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= + dependencies: + find-up "^2.0.0" + load-json-file "^4.0.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" + integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +prelude-ls@^1.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== + +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= + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + 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.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +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" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +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== + +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.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + 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.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.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +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" + +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-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== + +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-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-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.20.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +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" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +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== + +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" + +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" + +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: + queue-microtask "^1.2.2" + +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.2.1, 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== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.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== + +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.7.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.5, semver@^7.3.8: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +seq-queue@^0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" + integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q== + +serve-static@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.0.tgz#2bf4ed49f8af311b519c46f272bf6ac3baf38a92" + integrity sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.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= + +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" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +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: + shebang-regex "^3.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== + +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: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +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== + +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +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" + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + +split2@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +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@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.6.tgz#1d4fbc90fe4fbd51e952e0a90fd8f6c2b9098e97" + integrity sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^4.2.0" + tar "^6.1.11" + optionalDependencies: + node-gyp "8.x" + +sqlstring@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" + integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +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@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +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: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-ansi@^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-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.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== + +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" + +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== + +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.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" + +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.1.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" + integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^4.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== + +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= + +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== + +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@^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" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +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" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +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: + prelude-ls "^1.2.1" + +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== + +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.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" + +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" + +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== + dependencies: + unique-slug "^2.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== + dependencies: + imurmurhash "^0.1.4" + +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= + +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" + +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" + +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== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.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.1, which@^2.0.2: + 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" + +wide-align@^1.1.2, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +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.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== + +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" + +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: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== + +yallist@^3.0.0, yallist@^3.1.1: + 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== + +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" + +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== + +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/bin/build b/bin/build deleted file mode 100755 index 43d749a6a..000000000 --- a/bin/build +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script build -exit $? diff --git a/bin/build-dev b/bin/build-dev deleted file mode 100755 index 25203eb97..000000000 --- a/bin/build-dev +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sudo /usr/local/bin/docker-compose run --no-deps --rm app npm run-script dev -exit $? diff --git a/bin/migrate_create b/bin/migrate_create deleted file mode 100755 index dc2b5edf6..000000000 --- a/bin/migrate_create +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -if [ "$1" == "" ]; then - echo "Error: migrate name must be specified as first arg" - exit 1 -else - # Code path - SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - if hash realpath 2>/dev/null; then - export CODEBASE=$(realpath $SCRIPT_DIR/..) - elif hash grealpath 2>/dev/null; then - export CODEBASE=$(grealpath $SCRIPT_DIR/..) - else - export CODEBASE=$(readlink -e $SCRIPT_DIR/..) - fi - - if [ -z "$CODEBASE" ]; then - echo "Unable to determine absolute codebase directory" - exit 1 - fi - - cd "$CODEBASE" - - sudo /usr/local/bin/docker-compose run --rm --no-deps app node node_modules/knex/bin/cli.js migrate:make "$1" - exit $? -fi diff --git a/bin/npm b/bin/npm deleted file mode 100755 index a0863df08..000000000 --- a/bin/npm +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sudo /usr/local/bin/docker-compose run --no-deps --rm app npm $@ -exit $? diff --git a/bin/yarn b/bin/yarn deleted file mode 100755 index e4fd807d4..000000000 --- a/bin/yarn +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -sudo /usr/local/bin/docker-compose run --no-deps --rm app yarn $@ -exit $? diff --git a/config/my.cnf b/config/my.cnf deleted file mode 100644 index e3860d701..000000000 --- a/config/my.cnf +++ /dev/null @@ -1,7 +0,0 @@ -[mysqld] -skip-innodb -default-storage-engine=Aria -default-tmp-storage-engine=Aria -innodb=OFF -symbolic-links=0 -log-output=file diff --git a/doc/ADVANCED_NGINX.md b/doc/ADVANCED_NGINX.md deleted file mode 100644 index eabac84c4..000000000 --- a/doc/ADVANCED_NGINX.md +++ /dev/null @@ -1,17 +0,0 @@ -## Advanced Nginx Configuration - -If you are a more advanced user, you might be itching for extra Nginx customizability. - -NPM has the ability to include different custom configuration snippets in different places. - -You can add your custom configuration snippet files at `/data/nginx/custom` as follow: - -`/data/nginx/custom/root.conf`: Included at the very end of nginx.conf -`/data/nginx/custom/http.conf`: Included at the end of the main http block -`/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block -`/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block -`/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block -`/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block -`/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block - -Every file is optional. \ No newline at end of file diff --git a/doc/DOCKERHUB.md b/doc/DOCKERHUB.md deleted file mode 100644 index 1aa1b439e..000000000 --- a/doc/DOCKERHUB.md +++ /dev/null @@ -1,52 +0,0 @@ -![Nginx Proxy Manager](https://public.jc21.com/nginx-proxy-manager/github.png "Nginx Proxy Manager") - -# [Nginx Proxy Manager](https://nginxproxymanager.jc21.com) - -![Version](https://img.shields.io/badge/version-2.0.14-green.svg?style=for-the-badge) -![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) -![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) - -[View on Github](https://github.com/jc21/nginx-proxy-manager) - -This project comes as a pre-built docker image that enables you to easily forward to your websites -running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. - - -## Tags - -* latest 2, 2.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile)) -* latest-arm64, 2-arm64, 2.x.x-arm64 ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.arm64)) -* latest-arm7l, 2-arm7l, 2.x.x-arm7l ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.arm7l)) - - -## Getting started - -Please consult the [installation instructions](https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md) for a complete guide or -if you just want to get up and running in the quickest time possible, grab all the files in the [doc/example/](https://github.com/jc21/nginx-proxy-manager/tree/master/doc/example) folder and run: - -```bash -docker-compose up -d -``` - - -## Screenshots - -[![Login](https://public.jc21.com/nginx-proxy-manager/v2/small/login.jpg "Login")](https://public.jc21.com/nginx-proxy-manager/v2/large/login.jpg) -[![Dashboard](https://public.jc21.com/nginx-proxy-manager/v2/small/dashboard.jpg "Dashboard")](https://public.jc21.com/nginx-proxy-manager/v2/large/dashboard.jpg) -[![Proxy Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts.jpg "Proxy Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts.jpg) -[![Proxy Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts-new1.jpg "Proxy Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts-new1.jpg) -[![Proxy Host SSL](https://public.jc21.com/nginx-proxy-manager/v2/small/proxy-hosts-new2.jpg "Proxy Host SSL")](https://public.jc21.com/nginx-proxy-manager/v2/large/proxy-hosts-new2.jpg) -[![Redirection Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/redirection-hosts.jpg "Redirection Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/redirection-hosts.jpg) -[![Redirection Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/redirection-hosts-new1.jpg "Redirection Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/redirection-hosts-new1.jpg) -[![Streams](https://public.jc21.com/nginx-proxy-manager/v2/small/streams.jpg "Streams")](https://public.jc21.com/nginx-proxy-manager/v2/large/streams.jpg) -[![Stream Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/streams-new1.jpg "Stream Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/streams-new1.jpg) -[![404 Hosts](https://public.jc21.com/nginx-proxy-manager/v2/small/dead-hosts.jpg "404 Hosts")](https://public.jc21.com/nginx-proxy-manager/v2/large/dead-hosts.jpg) -[![404 Host Settings](https://public.jc21.com/nginx-proxy-manager/v2/small/dead-hosts-new1.jpg "404 Host Settings")](https://public.jc21.com/nginx-proxy-manager/v2/large/dead-hosts-new1.jpg) -[![Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates.jpg "Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates.jpg) -[![Lets Encrypt Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates-new1.jpg "Lets Encrypt Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates-new1.jpg) -[![Custom Certificates](https://public.jc21.com/nginx-proxy-manager/v2/small/certificates-new2.jpg "Custom Certificates")](https://public.jc21.com/nginx-proxy-manager/v2/large/certificates-new2.jpg) -[![Access Lists](https://public.jc21.com/nginx-proxy-manager/v2/small/access-lists.jpg "Access Lists")](https://public.jc21.com/nginx-proxy-manager/v2/large/access-lists.jpg) -[![Access List Users](https://public.jc21.com/nginx-proxy-manager/v2/small/access-lists-new1.jpg "Access List Users")](https://public.jc21.com/nginx-proxy-manager/v2/large/access-lists-new1.jpg) -[![Users](https://public.jc21.com/nginx-proxy-manager/v2/small/users.jpg "Users")](https://public.jc21.com/nginx-proxy-manager/v2/large/users.jpg) -[![User Permissions](https://public.jc21.com/nginx-proxy-manager/v2/small/users-permissions.jpg "User Permissions")](https://public.jc21.com/nginx-proxy-manager/v2/large/users-permissions.jpg) -[![Audit Log](https://public.jc21.com/nginx-proxy-manager/v2/small/audit-log.jpg "Audit Log")](https://public.jc21.com/nginx-proxy-manager/v2/large/audit-log.jpg) diff --git a/doc/IMPORTING.md b/doc/IMPORTING.md deleted file mode 100644 index bfda51649..000000000 --- a/doc/IMPORTING.md +++ /dev/null @@ -1,57 +0,0 @@ -## Importing from Version 1 - -Thanks for using Nginx Proxy Manager version 1. It sucked. - -But it worked. - -This guide will let your import your configuration from version 1 to version 2. - -**IMPORTANT: This will make changes to your `letsencrypt` folder and certificate files!** Make sure you back them up first. - - -### Link your previous folders in your new docker stack - -In version 1, the docker configuration asked for a `config` folder to be linked and a `letsencrypt` folder. However in version 2, the -configuration exists in the database, so the `config` folder is no longer required. However if you have this folder linked in a -version 2 stack, the application will automatically import that configuration the first time it finds it. - -Following the [example configuration](../example): - -```yaml -version: "3" -services: - app: - image: jc21/nginx-proxy-manager:2 - restart: always - ports: - - 80:80 - - 81:81 - - 443:443 - volumes: - - ./config.json:/app/config/production.json - - ./data:/data - - ./letsencrypt:/etc/letsencrypt # this is your previous letsencrypt folder - - ./config:/config # this is your previous config folder - depends_on: - - db - db: - image: mariadb - restart: always - environment: - MYSQL_ROOT_PASSWORD: "password123" - MYSQL_DATABASE: "nginxproxymanager" - MYSQL_USER: "nginxproxymanager" - MYSQL_PASSWORD: "password123" - volumes: - - ./data/mysql:/var/lib/mysql -``` - -After you start the stack, the import will begin just after database initialize. - -Some notes: -- After importing, a file is created in the `config` folder to signify that it has been imported and should not be imported again. -- Because no users previously existed in the version 1 config, the `admin@example.com` user will own all of the imported data. -- If you were crazy like me and used Nginx Proxy Manager version 1 to proxy the Admin interface behind a Access List, you should -really disable the access list for that proxy host in version 1 before importing in to version 2. The app doesn't like being behind basic -authentication and it's own internal authentication. If you forgot to do this before importing, just hit the admin interface directly -on port 81 to get around your basic authentication access list. diff --git a/doc/INSTALL.md b/doc/INSTALL.md deleted file mode 100644 index ad74c08d1..000000000 --- a/doc/INSTALL.md +++ /dev/null @@ -1,147 +0,0 @@ -## Installation and Configuration - -If you just want to get up and running in the quickest time possible, grab all the files in -the [doc/example/](https://github.com/jc21/nginx-proxy-manager/tree/master/doc/example) -folder and run: - -```bash -docker-compose up -d -``` - - -### Configuration File - -**The configuration file needs to be provided by you!** - -Don't worry, this is easy to do. - -The app requires a configuration file to let it know what database you're using. - -Here's an example configuration for `mysql` (or mariadb) that is compatible with the docker-compose example below: - -```json -{ - "database": { - "engine": "mysql", - "host": "db", - "name": "npm", - "user": "npm", - "password": "npm", - "port": 3306 - } -} -``` - -Once you've created your configuration file it's easy to mount it in the docker container. - -**Note:** After the first run of the application, the config file will be altered to include generated encryption keys unique to your installation. These keys -affect the login and session management of the application. If these keys change for any reason, all users will be logged out. - - -### Database - -This app doesn't come with a database, you have to provide one yourself. Currently only `mysql/mariadb` is supported for the minimum versions: - -- MySQL v5.7.8+ -- MariaDB v10.2.7+ - -It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples -are going to use. - - -### Running the App - -Via `docker-compose`: - -```yml -version: "3" -services: - app: - image: jc21/nginx-proxy-manager:latest - restart: always - ports: - # Public HTTP Port: - - 80:80 - # Public HTTPS Port: - - 443:443 - # Admin Web Port: - - 81:81 - volumes: - # Make sure this config.json file exists as per instructions above: - - ./config.json:/app/config/production.json - - ./data:/data - - ./letsencrypt:/etc/letsencrypt - depends_on: - - db - db: - image: mariadb - restart: always - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" - volumes: - - ./data/mysql:/var/lib/mysql -``` - -Then: - -```bash -docker-compose up -d -``` - - -### Running on Raspberry PI / ARM devices - -There are docker images for all versions of the Rasberry Pi with the exception of the really old `armv6l` versions. - -The `latest` docker image is a manifest of all the different architecture docker builds supported, so this means -you don't have to worry about doing anything special and you can follow the common instructions above. - -Check out the [dockerhub tags](https://cloud.docker.com/repository/registry-1.docker.io/jc21/nginx-proxy-manager/tags) -for a list of supported architectures and if you want one that doesn't exist, -[create a feature request](https://github.com/jc21/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). - -Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) -on Raspbian. - - -### Initial Run - -After the app is running for the first time, the following will happen: - -- The database will initialize with table structures -- GPG keys will be generated and saved in the configuration file -- A default admin user will be created - -This process can take a couple of minutes depending on your machine. - - -### Default Administrator User - -``` -Email: admin@example.com -Password: changeme -``` - -Immediately after logging in with this default user you will be asked to modify your details and change your password. - - -### Advanced Options - -#### X-FRAME-OPTIONS Header - -You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header -value by specifying it as a Docker environment variable. The default if not specified is `deny`. - -```yml - ... - environment: - X_FRAME_OPTIONS: "sameorigin" - ... -``` - -``` -... -e "X_FRAME_OPTIONS=sameorigin" ... -``` diff --git a/doc/example/config.json b/doc/example/config.json deleted file mode 100644 index 64ab577c8..000000000 --- a/doc/example/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "database": { - "engine": "mysql", - "host": "db", - "name": "npm", - "user": "npm", - "password": "npm", - "port": 3306 - } -} diff --git a/doc/example/docker-compose.yml b/doc/example/docker-compose.yml deleted file mode 100644 index e6707b957..000000000 --- a/doc/example/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: "3" -services: - app: - image: jc21/nginx-proxy-manager:latest - restart: always - ports: - - 80:80 - - 81:81 - - 443:443 - volumes: - - ./config.json:/app/config/production.json - - ./data:/data - - ./letsencrypt:/etc/letsencrypt - depends_on: - - db - environment: - # if you want pretty colors in your docker logs: - - FORCE_COLOR=1 - db: - image: mariadb:latest - restart: always - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" - volumes: - - ./data/mysql:/var/lib/mysql diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ab50c910a..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. -version: "2" -services: - app: - image: jc21/nginx-proxy-manager-base:latest - ports: - - 80:80 - - 81:81 - - 43:443 - environment: - - NODE_ENV=development - - FORCE_COLOR=1 - volumes: - - ./data:/data - - ./data/letsencrypt:/etc/letsencrypt - - .:/app - - ./rootfs/etc/nginx:/etc/nginx - working_dir: /app - depends_on: - - db - links: - - db - command: node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js - db: - image: jc21/mariadb-aria - environment: - MYSQL_ROOT_PASSWORD: "npm" - MYSQL_DATABASE: "npm" - MYSQL_USER: "npm" - MYSQL_PASSWORD: "npm" - volumes: - - ./config/my.cnf:/etc/mysql/conf.d/npm.cnf - - ./data/mysql:/var/lib/mysql diff --git a/docker/.dive-ci b/docker/.dive-ci new file mode 100644 index 000000000..7a408bdf2 --- /dev/null +++ b/docker/.dive-ci @@ -0,0 +1,14 @@ +rules: + # If the efficiency is measured below X%, mark as failed. + # Expressed as a ratio between 0-1. + lowestEfficiency: 0.99 + + # If the amount of wasted space is at least X or larger than X, mark as failed. + # Expressed in B, KB, MB, and GB. + highestWastedBytes: 15MB + + # If the amount of wasted space makes up for X% or more of the image, mark as failed. + # Note: the base image layer is NOT included in the total image size. + # Expressed as a ratio between 0-1; fails if the threshold is met or crossed. + highestUserWastedPercent: 0.02 + diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..0603e2ded --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,66 @@ +# 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 + +FROM nginxproxymanager/testca AS testca +FROM letsencrypt/pebble AS pebbleca +FROM nginxproxymanager/nginx-full:certbot-node + +ARG TARGETPLATFORM +ARG BUILD_VERSION +ARG BUILD_COMMIT +ARG BUILD_DATE + +# See: https://github.com/just-containers/s6-overlay/blob/master/README.md +ENV SUPPRESS_NO_CONFIG_WARNING=1 \ + 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=1 \ + NODE_ENV=production \ + NPM_BUILD_VERSION="${BUILD_VERSION}" \ + NPM_BUILD_COMMIT="${BUILD_COMMIT}" \ + NPM_BUILD_DATE="${BUILD_DATE}" \ + NODE_OPTIONS="--openssl-legacy-provider" + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ + && apt-get update \ + && apt-get install -y --no-install-recommends jq logrotate \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# s6 overlay +COPY docker/scripts/install-s6 /tmp/install-s6 +RUN /tmp/install-s6 "${TARGETPLATFORM}" && rm -f /tmp/install-s6 + +EXPOSE 80 81 443 + +COPY backend /app +COPY frontend/dist /app/frontend +COPY global /app/global + +WORKDIR /app +RUN yarn install \ + && yarn cache clean + +# add late to limit cache-busting by modifications +COPY docker/rootfs / +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 + +# Remove frontend service not required for prod, dev nginx config as well +RUN rm -rf /etc/s6-overlay/s6-rc.d/user/contents.d/frontend /etc/nginx/conf.d/dev.conf \ + && chmod 644 /etc/logrotate.d/nginx-proxy-manager + +VOLUME [ "/data" ] +ENTRYPOINT [ "/init" ] + +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" 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 new file mode 100644 index 000000000..dcb1f1f9b --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,40 @@ +FROM nginxproxymanager/testca AS testca +FROM letsencrypt/pebble AS pebbleca +FROM nginxproxymanager/nginx-full:certbot-node +LABEL maintainer="Jamie Curnow " + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ENV SUPPRESS_NO_CONFIG_WARNING=1 \ + 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 \ + NODE_OPTIONS="--openssl-legacy-provider" + +RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ + && apt-get update \ + && apt-get install -y jq python3-pip logrotate \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Task +WORKDIR /usr +RUN curl -sL https://taskfile.dev/install.sh | sh +WORKDIR /root + +COPY rootfs / +COPY scripts/install-s6 /tmp/install-s6 +RUN rm -f /etc/nginx/conf.d/production.conf \ + && chmod 644 /etc/logrotate.d/nginx-proxy-manager \ + && /tmp/install-s6 "${TARGETPLATFORM}" \ + && rm -f /tmp/install-s6 \ + && chmod 644 -R /root/.cache + +# Certs for testing purposes +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 81 443 +ENTRYPOINT [ "/init" ] diff --git a/docker/dev/dnsrouter-config.json b/docker/dev/dnsrouter-config.json new file mode 100644 index 000000000..a4e538d79 --- /dev/null +++ b/docker/dev/dnsrouter-config.json @@ -0,0 +1,28 @@ +{ + "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" + } + ] +} diff --git a/docker/dev/letsencrypt.ini b/docker/dev/letsencrypt.ini new file mode 100644 index 000000000..93647b648 --- /dev/null +++ b/docker/dev/letsencrypt.ini @@ -0,0 +1,7 @@ +text = True +non-interactive = True +webroot-path = /data/letsencrypt-acme-challenge +key-type = ecdsa +elliptic-curve = secp384r1 +preferred-chain = ISRG Root X1 +server = 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..ea9379055 --- /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 + } +} diff --git a/docker/dev/squid.conf b/docker/dev/squid.conf new file mode 100644 index 000000000..cdd749f58 --- /dev/null +++ b/docker/dev/squid.conf @@ -0,0 +1,92 @@ +# WELCOME TO SQUID 6.6 +# ---------------------------- +# +# This is the documentation for the Squid configuration file. +# This documentation can also be found online at: +# http://www.squid-cache.org/Doc/config/ +# +# You may wish to look at the Squid home page and wiki for the +# FAQ and other documentation: +# http://www.squid-cache.org/ +# https://wiki.squid-cache.org/SquidFaq +# https://wiki.squid-cache.org/ConfigExamples +# + +# Example rule allowing access from your local networks. +# Adapt to list your (internal) IP networks from where browsing +# should be allowed +acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) +acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) +acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) +acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines +acl localnet src 172.0.0.0/8 +acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) +acl localnet src fc00::/7 # RFC 4193 local private network range +acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + +acl SSL_ports port 443 +acl Safe_ports port 80 # http +acl Safe_ports port 81 +acl Safe_ports port 443 # https + +# +# Recommended minimum Access Permission configuration: +# +# Deny requests to certain unsafe ports +http_access deny !Safe_ports + +# Deny CONNECT to other than secure SSL ports +http_access deny CONNECT !SSL_ports + +# Only allow cachemgr access from localhost +http_access allow localhost manager +http_access deny manager + +# This default configuration only allows localhost requests because a more +# permissive Squid installation could introduce new attack vectors into the +# network by proxying external TCP connections to unprotected services. +http_access allow localhost + +# The two deny rules below are unnecessary in this default configuration +# because they are followed by a "deny all" rule. However, they may become +# critically important when you start allowing external requests below them. + +# Protect web applications running on the same server as Squid. They often +# assume that only local users can access them at "localhost" ports. +http_access deny to_localhost + +# Protect cloud servers that provide local users with sensitive info about +# their server via certain well-known link-local (a.k.a. APIPA) addresses. +http_access deny to_linklocal + +# +# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS +# +include /etc/squid/conf.d/*.conf + +# For example, to allow access from your local networks, you may uncomment the +# following rule (and/or add rules that match your definition of "local"): +# http_access allow localnet + +# And finally deny all other access to this proxy +http_access deny all + +# Squid normally listens to port 3128 +http_port 3128 + +# Leave coredumps in the first cache dir +coredump_dir /var/spool/squid + +# +# Add any of your own refresh_pattern entries above these. +# +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims +refresh_pattern \/InRelease$ 0 0% 0 refresh-ims +refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims +# example pattern for deb packages +#refresh_pattern (\.deb|\.udeb)$ 129600 100% 129600 +refresh_pattern . 0 20% 4320 + diff --git a/docker/docker-compose.ci.mysql.yml b/docker/docker-compose.ci.mysql.yml new file mode 100644 index 000000000..108a1dca3 --- /dev/null +++ b/docker/docker-compose.ci.mysql.yml @@ -0,0 +1,28 @@ +# 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: + environment: + DB_MYSQL_HOST: 'db-mysql' + DB_MYSQL_PORT: '3306' + DB_MYSQL_USER: 'npm' + DB_MYSQL_PASSWORD: 'npmpass' + DB_MYSQL_NAME: 'npm' + depends_on: + - db-mysql + + db-mysql: + image: jc21/mariadb-aria + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npmpass' + MARIADB_AUTO_UPGRADE: '1' + volumes: + - mysql_vol:/var/lib/mysql + networks: + - fulltest + +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..c4468c68b --- /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: + DB_POSTGRES_HOST: 'db-postgres' + DB_POSTGRES_PORT: '5432' + DB_POSTGRES_USER: 'npm' + DB_POSTGRES_PASSWORD: 'npmpass' + DB_POSTGRES_NAME: 'npm' + 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 + networks: + - fulltest + + 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.sqlite.yml b/docker/docker-compose.ci.sqlite.yml new file mode 100644 index 000000000..1c5be48e9 --- /dev/null +++ b/docker/docker-compose.ci.sqlite.yml @@ -0,0 +1,9 @@ +# 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: + environment: + DB_SQLITE_FILE: '/data/mydb.sqlite' + PUID: 1000 + PGID: 1000 + DISABLE_IPV6: 'true' diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml new file mode 100644 index 000000000..280a05465 --- /dev/null +++ b/docker/docker-compose.ci.yml @@ -0,0 +1,128 @@ +# WARNING: This is a CI docker-compose file used for building +# and testing of the entire app, it should not be used for production. +# This is a base compose file, it should be extended with a +# docker-compose.ci.*.yml file +services: + + fullstack: + image: "${IMAGE}:${BRANCH_LOWER}-ci-${BUILD_NUMBER}" + environment: + DEBUG: 'true' + FORCE_COLOR: 1 + # Required for DNS Certificate provisioning in CI + LE_SERVER: 'https://ca.internal/acme/acme/directory' + REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt' + volumes: + - 'npm_data_ci:/data' + - 'npm_le_ci:/etc/letsencrypt' + - './dev/letsencrypt.ini:/etc/letsencrypt.ini:ro' + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s + expose: + - '80-81/tcp' + - '443/tcp' + - '1500-1503/tcp' + networks: + fulltest: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com + + stepca: + image: jc21/testca + volumes: + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + networks: + fulltest: + aliases: + - ca.internal + + pdns: + image: pschiffe/pdns-mysql:4.8 + 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: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + fulltest: + aliases: + - ns1.pdns + - ns2.pdns + + pdns-db: + image: mariadb + environment: + 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' + networks: + - fulltest + + dnsrouter: + image: jc21/dnsrouter + volumes: + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + networks: + - fulltest + + cypress: + image: "${IMAGE}-cypress:ci-${BUILD_NUMBER}" + build: + context: ../ + dockerfile: test/cypress/Dockerfile + environment: + HTTP_PROXY: 'squid:3128' + HTTPS_PROXY: 'squid:3128' + volumes: + - 'cypress_logs:/test/results' + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + command: cypress run --browser chrome --config-file=cypress/config/ci.js + networks: + - fulltest + + squid: + image: ubuntu/squid + volumes: + - './dev/squid.conf:/etc/squid/squid.conf:ro' + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + networks: + - fulltest + +volumes: + cypress_logs: + npm_data_ci: + npm_le_ci: + pdns_mysql_vol: + +networks: + fulltest: + name: "npm-${BRANCH_LOWER}-ci-${BUILD_NUMBER}" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml new file mode 100644 index 000000000..5abe057b0 --- /dev/null +++ b/docker/docker-compose.dev.yml @@ -0,0 +1,268 @@ +# WARNING: This is a DEVELOPMENT docker-compose file, it should not be used for production. +services: + + fullstack: + image: npm2dev:core + container_name: npm2dev.core + build: + context: ./ + dockerfile: ./dev/Dockerfile + ports: + - 3080:80 + - 3081:81 + - 3443:443 + networks: + nginx_proxy_manager: + aliases: + - website1.example.com + - website2.example.com + - website3.example.com + environment: + PUID: 1000 + PGID: 1000 + FORCE_COLOR: 1 + # specifically for dev: + DEBUG: 'true' + DEVELOPMENT: 'true' + LE_STAGING: 'true' + # db: + # DB_MYSQL_HOST: 'db' + # DB_MYSQL_PORT: '3306' + # DB_MYSQL_USER: 'npm' + # DB_MYSQL_PASSWORD: 'npm' + # DB_MYSQL_NAME: 'npm' + # db-postgres: + DB_POSTGRES_HOST: 'db-postgres' + DB_POSTGRES_PORT: '5432' + DB_POSTGRES_USER: 'npm' + DB_POSTGRES_PASSWORD: 'npmpass' + DB_POSTGRES_NAME: 'npm' + # DB_SQLITE_FILE: "/data/database.sqlite" + # DISABLE_IPV6: "true" + # Required for DNS Certificate provisioning testing: + LE_SERVER: 'https://ca.internal/acme/acme/directory' + REQUESTS_CA_BUNDLE: '/etc/ssl/certs/NginxProxyManager.crt' + volumes: + - npm_data:/data + - le_data:/etc/letsencrypt + - './dev/resolv.conf:/etc/resolv.conf:ro' + - ../backend:/app + - ../frontend:/app/frontend + - ../global:/app/global + healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s + depends_on: + - db + - db-postgres + - authentik + - authentik-worker + - authentik-ldap + working_dir: /app + + db: + image: jc21/mariadb-aria + container_name: npm2dev.db + ports: + - 33306:3306 + networks: + - nginx_proxy_manager + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npm' + volumes: + - db_data:/var/lib/mysql + + db-postgres: + image: postgres:latest + container_name: npm2dev.db-postgres + networks: + - nginx_proxy_manager + environment: + POSTGRES_USER: 'npm' + POSTGRES_PASSWORD: 'npmpass' + POSTGRES_DB: 'npm' + volumes: + - psql_data:/var/lib/postgresql/data + - ./ci/postgres:/docker-entrypoint-initdb.d + + stepca: + image: jc21/testca + container_name: npm2dev.stepca + volumes: + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + networks: + nginx_proxy_manager: + aliases: + - ca.internal + + dnsrouter: + image: jc21/dnsrouter + container_name: npm2dev.dnsrouter + volumes: + - ./dev/dnsrouter-config.json.tmp:/dnsrouter-config.json:ro + networks: + - nginx_proxy_manager + + swagger: + image: swaggerapi/swagger-ui:latest + container_name: npm2dev.swagger + ports: + - 3082:80 + environment: + URL: "http://npm:81/api/schema" + PORT: '80' + depends_on: + - fullstack + + squid: + image: ubuntu/squid + container_name: npm2dev.squid + volumes: + - './dev/squid.conf:/etc/squid/squid.conf:ro' + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + networks: + - nginx_proxy_manager + ports: + - 8128:3128 + + pdns: + image: pschiffe/pdns-mysql:4.8 + container_name: npm2dev.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: pdns-db + PDNS_gmysql_port: 3306 + PDNS_gmysql_user: pdns + PDNS_gmysql_password: pdns + PDNS_gmysql_dbname: pdns + depends_on: + - pdns-db + networks: + nginx_proxy_manager: + aliases: + - ns1.pdns + - ns2.pdns + + pdns-db: + image: mariadb + container_name: npm2dev.pdns-db + environment: + MYSQL_ROOT_PASSWORD: 'pdns' + MYSQL_DATABASE: 'pdns' + MYSQL_USER: 'pdns' + MYSQL_PASSWORD: 'pdns' + volumes: + - 'pdns_mysql:/var/lib/mysql' + - '/etc/localtime:/etc/localtime:ro' + - './dev/pdns-db.sql:/docker-entrypoint-initdb.d/01_init.sql:ro' + networks: + - nginx_proxy_manager + + cypress: + image: npm2dev:cypress + container_name: npm2dev.cypress + build: + context: ../ + dockerfile: test/cypress/Dockerfile + environment: + HTTP_PROXY: 'squid:3128' + HTTPS_PROXY: 'squid:3128' + volumes: + - '../test/results:/results' + - './dev/resolv.conf:/etc/resolv.conf:ro' + - '/etc/localtime:/etc/localtime:ro' + command: cypress run --browser chrome --config-file=cypress/config/ci.js + networks: + - nginx_proxy_manager + + authentik-redis: + image: 'redis:alpine' + container_name: npm2dev.authentik-redis + command: --save 60 1 --loglevel warning + networks: + - nginx_proxy_manager + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli ping | grep PONG'] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis_data:/data + + authentik: + image: ghcr.io/goauthentik/server:2024.10.1 + container_name: npm2dev.authentik + restart: unless-stopped + command: server + networks: + - nginx_proxy_manager + env_file: + - ci.env + ports: + - 9000:9000 + depends_on: + - authentik-redis + - db-postgres + + authentik-worker: + image: ghcr.io/goauthentik/server:2024.10.1 + container_name: npm2dev.authentik-worker + restart: unless-stopped + command: worker + networks: + - nginx_proxy_manager + env_file: + - ci.env + depends_on: + - authentik-redis + - db-postgres + + authentik-ldap: + image: ghcr.io/goauthentik/ldap:2024.10.1 + container_name: npm2dev.authentik-ldap + networks: + - nginx_proxy_manager + environment: + AUTHENTIK_HOST: 'http://authentik:9000' + AUTHENTIK_INSECURE: 'true' + AUTHENTIK_TOKEN: 'wKYZuRcI0ETtb8vWzMCr04oNbhrQUUICy89hSpDln1OEKLjiNEuQ51044Vkp' + restart: unless-stopped + depends_on: + - authentik + +volumes: + npm_data: + name: npm2dev_core_data + le_data: + name: npm2dev_le_data + db_data: + name: npm2dev_db_data + pdns_mysql: + name: npnpm2dev_pdns_mysql + psql_data: + name: npm2dev_psql_data + redis_data: + name: npm2dev_redis_data + +networks: + nginx_proxy_manager: + name: npm2dev_network diff --git a/rootfs/root/.config/letsencrypt/cli.ini b/docker/rootfs/etc/letsencrypt.ini similarity index 52% rename from rootfs/root/.config/letsencrypt/cli.ini rename to docker/rootfs/etc/letsencrypt.ini index 3565d6e5c..aae53b902 100644 --- a/rootfs/root/.config/letsencrypt/cli.ini +++ b/docker/rootfs/etc/letsencrypt.ini @@ -1,4 +1,6 @@ text = True non-interactive = True -authenticator = webroot webroot-path = /data/letsencrypt-acme-challenge +key-type = ecdsa +elliptic-curve = secp384r1 +preferred-chain = ISRG Root X1 diff --git a/docker/rootfs/etc/logrotate.d/nginx-proxy-manager b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager new file mode 100644 index 000000000..de9772971 --- /dev/null +++ b/docker/rootfs/etc/logrotate.d/nginx-proxy-manager @@ -0,0 +1,27 @@ +/data/logs/*_access.log /data/logs/*/access.log { + su npm npm + create 0644 + weekly + rotate 4 + missingok + notifempty + compress + sharedscripts + postrotate + kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true + endscript +} + +/data/logs/*_error.log /data/logs/*/error.log { + su npm npm + create 0644 + weekly + rotate 10 + missingok + notifempty + compress + sharedscripts + postrotate + kill -USR1 `cat /run/nginx/nginx.pid 2>/dev/null` 2>/dev/null || true + endscript +} diff --git a/docker/rootfs/etc/nginx/conf.d/default.conf b/docker/rootfs/etc/nginx/conf.d/default.conf new file mode 100644 index 000000000..e4262e1dc --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/default.conf @@ -0,0 +1,39 @@ +# "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; + include conf.d/include/block-exploits.conf; + include conf.d/include/letsencrypt-acme-challenge.conf; + + location / { + index index.html; + root /var/www/html; + } +} + +# First 443 Host, which is the default if another default doesn't exist +server { + listen 443 ssl; + listen [::]:443 ssl; + + set $forward_scheme "https"; + set $server "127.0.0.1"; + set $port "443"; + + server_name localhost; + access_log /data/logs/fallback_access.log standard; + error_log /dev/null crit; + include conf.d/include/ssl-ciphers.conf; + 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 new file mode 100644 index 000000000..edbdec8ac --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/dev.conf @@ -0,0 +1,29 @@ +server { + listen 81 default; + listen [::]:81 default; + + server_name nginxproxymanager-dev; + root /app/frontend/dist; + 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; + try_files $uri $uri.html $uri/ /index.html; + } +} diff --git a/docker/rootfs/etc/nginx/conf.d/include/.gitignore b/docker/rootfs/etc/nginx/conf.d/include/.gitignore new file mode 100644 index 000000000..5291fe15e --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/.gitignore @@ -0,0 +1 @@ +resolvers.conf diff --git a/docker/rootfs/etc/nginx/conf.d/include/assets.conf b/docker/rootfs/etc/nginx/conf.d/include/assets.conf new file mode 100644 index 000000000..5a90beb8a --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/assets.conf @@ -0,0 +1,31 @@ +location ~* ^.*\.(css|js|jpe?g|gif|png|webp|woff|woff2|eot|ttf|svg|ico|css\.map|js\.map)$ { + if_modified_since off; + + # 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; + + # 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; + + 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; + + expires @30m; + access_log off; + + include conf.d/include/proxy.conf; +} diff --git a/rootfs/etc/nginx/conf.d/include/block-exploits.conf b/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf similarity index 68% rename from rootfs/etc/nginx/conf.d/include/block-exploits.conf rename to docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf index 22360fc1a..093bda235 100644 --- a/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 new file mode 100644 index 000000000..aa52f335d --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/force-ssl.conf @@ -0,0 +1,10 @@ +set $test ""; +if ($scheme = "http") { + set $test "H"; +} +if ($request_uri = /.well-known/acme-challenge/test-challenge) { + set $test "${test}T"; +} +if ($test = H) { + 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 new file mode 100644 index 000000000..342493254 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ip_ranges.conf @@ -0,0 +1,2 @@ +# 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 new file mode 100644 index 000000000..ff2a78274 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/letsencrypt-acme-challenge.conf @@ -0,0 +1,30 @@ +# 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/log.conf b/docker/rootfs/etc/nginx/conf.d/include/log.conf new file mode 100644 index 000000000..0d7402906 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/log.conf @@ -0,0 +1,4 @@ +log_format proxy '[$time_local] $upstream_cache_status $upstream_status $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] [Sent-to $server] "$http_user_agent" "$http_referer"'; +log_format standard '[$time_local] $status - $request_method $scheme $host "$request_uri" [Client $remote_addr] [Length $body_bytes_sent] [Gzip $gzip_ratio] "$http_user_agent" "$http_referer"'; + +access_log /data/logs/fallback_access.log proxy; diff --git a/docker/rootfs/etc/nginx/conf.d/include/proxy.conf b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf new file mode 100644 index 000000000..d346c4ef3 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/proxy.conf @@ -0,0 +1,8 @@ +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 $proxy_add_x_forwarded_for; +proxy_set_header X-Real-IP $remote_addr; +proxy_pass $forward_scheme://$server:$port$request_uri; + diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf new file mode 100644 index 000000000..433555dfa --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache-stream.conf @@ -0,0 +1,2 @@ +ssl_session_timeout 5m; +ssl_session_cache shared:SSL_stream:50m; diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf new file mode 100644 index 000000000..aa7ba2cb7 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-cache.conf @@ -0,0 +1,2 @@ +ssl_session_timeout 5m; +ssl_session_cache shared:SSL:50m; diff --git a/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf new file mode 100644 index 000000000..b5dacfb57 --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/include/ssl-ciphers.conf @@ -0,0 +1,4 @@ +# intermediate configuration. tweak to your needs. +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; +ssl_prefer_server_ciphers off; diff --git a/docker/rootfs/etc/nginx/conf.d/production.conf b/docker/rootfs/etc/nginx/conf.d/production.conf new file mode 100644 index 000000000..877e51dda --- /dev/null +++ b/docker/rootfs/etc/nginx/conf.d/production.conf @@ -0,0 +1,33 @@ +# 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; + } +} diff --git a/rootfs/etc/nginx/mime.types b/docker/rootfs/etc/nginx/mime.types similarity index 100% rename from rootfs/etc/nginx/mime.types rename to docker/rootfs/etc/nginx/mime.types diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf new file mode 100644 index 000000000..3a31e14ce --- /dev/null +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -0,0 +1,96 @@ +# run nginx in foreground +daemon off; +pid /run/nginx/nginx.pid; +user npm; + +# Set number of worker processes automatically based on number of CPU cores. +worker_processes auto; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +error_log /data/logs/fallback_error.log warn; + +# Includes files with directives to load dynamic modules. +include /etc/nginx/modules/*.conf; + +# Custom +include /data/nginx/custom/root_top[.]conf; + +events { + include /data/nginx/custom/events[.]conf; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + 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; + ssl_prefer_server_ciphers on; + gzip on; + proxy_ignore_client_abort off; + client_max_body_size 2000m; + server_names_hash_bucket_size 1024; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Accept-Encoding ""; + proxy_cache off; + 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 and fallback log file + include /etc/nginx/conf.d/include/log.conf; + + # Dynamically generated resolvers file + include /etc/nginx/conf.d/include/resolvers.conf; + + # Default upstream scheme + map $host $forward_scheme { + default http; + } + + # Real IP Determination + + # Local subnets: + set_real_ip_from 10.0.0.0/8; + set_real_ip_from 172.16.0.0/12; # Includes Docker subnet + set_real_ip_from 192.168.0.0/16; + # NPM generated CDN ip ranges: + include conf.d/include/ip_ranges.conf; + # always put the following 2 lines after ip subnets: + real_ip_header X-Real-IP; + real_ip_recursive on; + + # Custom + include /data/nginx/custom/http_top[.]conf; + + # Files generated by NPM + include /etc/nginx/conf.d/*.conf; + include /data/nginx/default_host/*.conf; + include /data/nginx/proxy_host/*.conf; + include /data/nginx/redirection_host/*.conf; + include /data/nginx/dead_host/*.conf; + include /data/nginx/temp/*.conf; + + # Custom + include /data/nginx/custom/http[.]conf; +} + +stream { + # Files generated by NPM + include /data/nginx/stream/*.conf; + + # Custom + include /data/nginx/custom/stream[.]conf; +} + +# Custom +include /data/nginx/custom/root[.]conf; diff --git a/docker/rootfs/etc/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..37a95f7d1 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run @@ -0,0 +1,21 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /usr/bin/common.sh + +cd /app || exit 1 + +log_info 'Starting backend ...' + +if [ "${DEVELOPMENT:-}" = 'true' ]; then + s6-setuidgid "$PUID:$PGID" yarn install + exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js" +else + while : + do + s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;node --abort_on_uncaught_exception --max_old_space_size=250 index.js" + sleep 1 + done +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/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..4f2035551 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run @@ -0,0 +1,21 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +# This service is DEVELOPMENT only. + +if [ "$DEVELOPMENT" = 'true' ]; then + . /usr/bin/common.sh + cd /app/frontend || exit 1 + HOME=$NPMHOME + export HOME + mkdir -p /app/frontend/dist + chown -R "$PUID:$PGID" /app/frontend/dist + + log_info 'Starting frontend ...' + s6-setuidgid "$PUID:$PGID" yarn install + exec s6-setuidgid "$PUID:$PGID" yarn watch +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..3e8d8d043 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -0,0 +1,9 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +. /usr/bin/common.sh + +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..d2e62f3bb --- /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 + +. /usr/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 [ "$DEBUG" = "true" ]; 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-ipv6.sh +. /etc/s6-overlay/s6-rc.d/prepare/60-secrets.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..2f59ef41a --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh @@ -0,0 +1,41 @@ +#!/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 +# Ensure /etc/letsencrypt is mounted +if [ ! -d '/etc/letsencrypt' ]; then + log_fatal '/etc/letsencrypt is not mounted! Check your docker configuration.' +fi + +# Create required folders +mkdir -p \ + /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 \ + /data/letsencrypt-acme-challenge \ + /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..fa9465189 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/30-ownership.sh @@ -0,0 +1,60 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +log_info 'Setting ownership ...' + +# root +chown root /tmp/nginx + +locations=( + "/data" + "/etc/letsencrypt" + "/run/nginx" + "/tmp/nginx" + "/var/cache/nginx" + "/var/lib/logrotate" + "/var/lib/nginx" + "/var/log/nginx" + "/etc/nginx/nginx" + "/etc/nginx/nginx.conf" + "/etc/nginx/conf.d" +) + +chownit() { + local dir="$1" + local recursive="${2:-true}" + + local have + have="$(stat -c '%u:%g' "$dir")" + echo "- $dir ... " + + if [ "$have" != "$PUID:$PGID" ]; then + if [ "$recursive" = 'true' ] && [ -d "$dir" ]; then + chown -R "$PUID:$PGID" "$dir" + else + chown "$PUID:$PGID" "$dir" + fi + echo " DONE" + else + echo " SKIPPED" + fi +} + +for loc in "${locations[@]}"; do + chownit "$loc" +done + +if [ "$(is_true "${SKIP_CERTBOT_OWNERSHIP:-}")" = '1' ]; then + log_info 'Skipping ownership change of certbot directories' +else + log_info 'Changing ownership of certbot directories, this may take some time ...' + chownit "/opt/certbot" false + chownit "/opt/certbot/bin" false + + # Handle all site-packages directories efficiently + find /opt/certbot/lib -type d -name "site-packages" | while read -r SITE_PACKAGES_DIR; do + chownit "$SITE_PACKAGES_DIR" + done +fi 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..e02f41ca1 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/40-dynamic.sh @@ -0,0 +1,14 @@ +#!/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 "$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 diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh new file mode 100755 index 000000000..2ae61ae55 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/50-ipv6.sh @@ -0,0 +1,36 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +# This command reads the `DISABLE_IPV6` env var and will either enable +# or disable ipv6 in all nginx configs based on this setting. + +set -e + +log_info 'IPv6 ...' + +process_folder () { + FILES=$(find "$1" -type f -name "*.conf") + SED_REGEX= + + if [ "$(is_true "$DISABLE_IPV6")" = '1' ]; then + # IPV6 is disabled + echo "Disabling IPV6 in hosts in: $1" + SED_REGEX='s/^([^#]*)listen \[::\]/\1#listen [::]/g' + else + # IPV6 is enabled + 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-secrets.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh new file mode 100755 index 000000000..faa22accb --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/60-secrets.sh @@ -0,0 +1,30 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e + +# in s6, environmental variables are written as text files for s6 to monitor +# search through full-path filenames for files ending in "__FILE" +log_info 'Docker secrets ...' + +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}") + # 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 "Success: ${STRIPFILE##*/} set from ${FILENAME##*/}" + + else + echo "Cannot find secret in ${FILENAME}" + fi +done 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..48ba63923 --- /dev/null +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/90-banner.sh @@ -0,0 +1,18 @@ +#!/command/with-contenv bash +# shellcheck shell=bash + +set -e +set +x + +echo " +------------------------------------- + _ _ ____ __ __ +| \ | | _ \| \/ | +| \| | |_) | |\/| | +| |\ | __/| | | | +|_| \_|_| |_| |_| +------------------------------------- +User: $NPMUSER PUID:$PUID ID:$(id -u "$NPMUSER") GROUP:$(id -g "$NPMUSER") +Group: $NPMGROUP PGID:$PGID ID:$(get_group_id "$NPMGROUP") +------------------------------------- +" 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/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/root/.bashrc b/docker/rootfs/root/.bashrc new file mode 100644 index 000000000..1deb975c0 --- /dev/null +++ b/docker/rootfs/root/.bashrc @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -t 1 ]; then + export PS1="\e[1;34m[\e[1;33m\u@\e[1;32mdocker-\h\e[1;37m:\w\[\e[1;34m]\e[1;36m\\$ \e[0m" +fi + +# Aliases +alias l='ls -lAsh --color' +alias ls='ls -C1 --color' +alias cp='cp -ip' +alias rm='rm -i' +alias mv='mv -i' +alias h='cd ~;clear;' + +. /etc/os-release + +echo -e -n '\E[1;34m' +figlet -w 120 "NginxProxyManager" +echo -e "\E[1;36mVersion \E[1;32m${NPM_BUILD_VERSION:-2.0.0-dev} (${NPM_BUILD_COMMIT:-dev}) ${NPM_BUILD_DATE:-0000-00-00}\E[1;36m, OpenResty \E[1;32m${OPENRESTY_VERSION:-unknown}\E[1;36m, ${ID:-debian} \E[1;32m${VERSION:-unknown}\E[1;36m, Certbot \E[1;32m$(certbot --version)\E[0m" +echo -e -n '\E[1;34m' +cat /built-for-arch +echo -e '\E[0m' diff --git a/docker/rootfs/usr/bin/check-health b/docker/rootfs/usr/bin/check-health new file mode 100755 index 000000000..bcf5552b3 --- /dev/null +++ b/docker/rootfs/usr/bin/check-health @@ -0,0 +1,11 @@ +#!/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/usr/bin/common.sh b/docker/rootfs/usr/bin/common.sh new file mode 100644 index 000000000..46529870a --- /dev/null +++ b/docker/rootfs/usr/bin/common.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +set -e + +CYAN='\E[1;36m' +BLUE='\E[1;34m' +YELLOW='\E[1;33m' +RED='\E[1;31m' +RESET='\E[0m' +export CYAN BLUE YELLOW RED RESET + +PUID=${PUID:-0} +PGID=${PGID:-0} + +# If changing the username and group name below, +# ensure all references to this user is also changed. +# See docker/rootfs/etc/logrotate.d/nginx-proxy-manager +# and docker/rootfs/etc/nginx/nginx.conf +NPMUSER=npm +NPMGROUP=npm +NPMHOME=/tmp/npmuserhome +export NPMUSER NPMGROUP NPMHOME + +if [[ "$PUID" -ne '0' ]] && [ "$PGID" = '0' ]; then + # set group id to same as user id, + # the user probably forgot to specify the group id and + # it would be rediculous to intentionally use the root group + # for a non-root user + PGID=$PUID +fi + +export PUID PGID + +log_info () { + echo -e "${BLUE}❯ ${CYAN}$1${RESET}" +} + +log_error () { + echo -e "${RED}❯ $1${RESET}" +} + +# The `run` file will only execute 1 line so this helps keep things +# logically separated + +log_fatal () { + echo -e "${RED}--------------------------------------${RESET}" + echo -e "${RED}ERROR: $1${RESET}" + echo -e "${RED}--------------------------------------${RESET}" + /run/s6/basedir/bin/halt + exit 1 +} + +# param $1: group_name +get_group_id () { + if [ "${1:-}" != '' ]; then + getent group "$1" | cut -d: -f3 + fi +} + +# param $1: value +is_true () { + VAL=$(echo "${1:-}" | tr '[:upper:]' '[:lower:]') + if [ "$VAL" == 'true' ] || [ "$VAL" == 'on' ] || [ "$VAL" == '1' ] || [ "$VAL" == 'yes' ]; then + echo '1' + else + echo '0' + fi +} diff --git a/rootfs/var/www/html/index.html b/docker/rootfs/var/www/html/index.html similarity index 95% rename from rootfs/var/www/html/index.html rename to docker/rootfs/var/www/html/index.html index 8478b47f1..703db883d 100644 --- a/rootfs/var/www/html/index.html +++ b/docker/rootfs/var/www/html/index.html @@ -5,7 +5,7 @@ Default Site - + diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6 new file mode 100755 index 000000000..639c65dd6 --- /dev/null +++ b/docker/scripts/install-s6 @@ -0,0 +1,38 @@ +#!/bin/bash -e + +# Note: This script is designed to be run inside a Docker Build for a container + +CYAN='\E[1;36m' +YELLOW='\E[1;33m' +BLUE='\E[1;34m' +GREEN='\E[1;32m' +RESET='\E[0m' + +S6_OVERLAY_VERSION=3.2.1.0 +TARGETPLATFORM=${1:-linux/amd64} + +# Determine the correct binary file for the architecture given +case $TARGETPLATFORM in + linux/arm64) + S6_ARCH=aarch64 + ;; + + linux/arm/v7) + S6_ARCH=armhf + ;; + + *) + 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-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/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..b8acd7be7 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,14 @@ +dist +node_modules +ts +.temp +.cache +.vitepress/cache + +.yarn/* +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions +*.gz +*.tgz diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 000000000..52586bfc3 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,61 @@ +import { defineConfig, type DefaultTheme } from 'vitepress'; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Nginx Proxy Manager", + description: "Expose your services easily and securely", + head: [ + ["link", { rel: "icon", href: "/icon.png" }], + ["meta", { name: "description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt" }], + ["meta", { property: "og:title", content: "Nginx Proxy Manager" }], + ["meta", { property: "og:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], + ["meta", { property: "og:type", content: "website" }], + ["meta", { property: "og:url", content: "https://nginxproxymanager.com/" }], + ["meta", { property: "og:image", content: "https://nginxproxymanager.com/icon.png" }], + ["meta", { name: "twitter:card", content: "summary"}], + ["meta", { name: "twitter:title", content: "Nginx Proxy Manager"}], + ["meta", { name: "twitter:description", content: "Docker container and built in Web Application for managing Nginx proxy hosts with a simple, powerful interface, providing free SSL support via Let's Encrypt"}], + ["meta", { name: "twitter:image", content: "https://nginxproxymanager.com/icon.png"}], + ["meta", { name: "twitter:alt", content: "Nginx Proxy Manager"}], + // GA + ['script', { async: 'true', src: 'https://www.googletagmanager.com/gtag/js?id=G-TXT8F5WY5B'}], + ['script', {}, "window.dataLayer = window.dataLayer || [];\nfunction gtag(){dataLayer.push(arguments);}\ngtag('js', new Date());\ngtag('config', 'G-TXT8F5WY5B');"], + ], + sitemap: { + hostname: 'https://nginxproxymanager.com' + }, + metaChunk: true, + srcDir: './src', + outDir: './dist', + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + logo: { src: '/logo.svg', width: 24, height: 24 }, + nav: [ + { text: 'Setup', link: '/setup/' }, + ], + sidebar: [ + { + items: [ + // { text: 'Home', link: '/' }, + { text: 'Guide', link: '/guide/' }, + { text: 'Screenshots', link: '/screenshots/' }, + { text: 'Setup Instructions', link: '/setup/' }, + { text: 'Advanced Configuration', link: '/advanced-config/' }, + { text: 'Upgrading', link: '/upgrading/' }, + { text: 'Frequently Asked Questions', link: '/faq/' }, + { text: 'Third Party', link: '/third-party/' }, + ] + } + ], + socialLinks: [ + { icon: 'github', link: 'https://github.com/NginxProxyManager/nginx-proxy-manager' } + ], + search: { + provider: 'local' + }, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2016-present jc21.com' + } + } +}); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 000000000..cd4852378 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,27 @@ +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #f15833 30%, #FAA42F); + + --vp-home-hero-image-background-image: linear-gradient(-45deg, #aaaaaa 50%, #777777 50%); + --vp-home-hero-image-filter: blur(44px); + + --vp-c-brand-1: #f15833; + --vp-c-brand-2: #FAA42F; + --vp-c-brand-3: #f15833; +} + + @media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + + @media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +.inline-img img { + display: inline; +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 000000000..42fe9a936 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,4 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default DefaultTheme diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..3e3dcba25 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,11 @@ +{ + "scripts": { + "dev": "vitepress dev --host", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.4.0" + }, + "dependencies": {} +} diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md new file mode 100644 index 000000000..4a7c260eb --- /dev/null +++ b/docs/src/advanced-config/index.md @@ -0,0 +1,230 @@ +--- +outline: deep +--- + +# Advanced Configuration + +## Running processes as a user/group + +By default, the services (nginx etc) will run as `root` user inside the docker container. +You can change this behaviour by setting the following environment variables. +Not only will they run the services as this user/group, they will change the ownership +on the `data` and `letsencrypt` folders at startup. + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + environment: + PUID: 1000 + PGID: 1000 + # ... +``` + +This may have the side effect of a failed container start due to permission denied trying +to open port 80 on some systems. The only course to fix that is to remove the variables +and run as the default root user. + +## Best Practice: Use a Docker network + +For those who have a few of their upstream services running in Docker on the same Docker +host as NPM, here's a trick to secure things a bit better. By creating a custom Docker network, +you don't need to publish ports for your upstream services to all of the Docker host's interfaces. + +Create a network, ie "scoobydoo": + +```bash +docker network create scoobydoo +``` + +Then add the following to the `docker-compose.yml` file for both NPM and any other +services running on this Docker host: + +```yml +networks: + default: + external: true + name: scoobydoo +``` + +Let's look at a Portainer example: + +```yml +services: + + portainer: + image: portainer/portainer + privileged: true + volumes: + - './data:/data' + - '/var/run/docker.sock:/var/run/docker.sock' + restart: unless-stopped + +networks: + default: + external: true + name: scoobydoo +``` + +Now in the NPM UI you can create a proxy host with `portainer` as the hostname, +and port `9000` as the port. Even though this port isn't listed in the docker-compose +file, it's "exposed" by the Portainer Docker image for you and not available on +the Docker host outside of this Docker network. The service name is used as the +hostname, so make sure your service names are unique when using the same network. + +## Docker Healthcheck + +The `Dockerfile` that builds this project does not include a `HEALTHCHECK` but you can opt in to this +feature by adding the following to the service in your `docker-compose.yml` file: + +```yml +healthcheck: + test: ["CMD", "/usr/bin/check-health"] + interval: 10s + timeout: 3s +``` + +## Docker File Secrets + +This image supports the use of Docker secrets to import from files and keep sensitive usernames or passwords from being passed or preserved in plaintext. + +You can set any environment variable from a file by appending `__FILE` (double-underscore FILE) to the environmental variable name. + +```yml +secrets: + # Secrets are single-line text files where the sole content is the secret + # Paths in this example assume that secrets are kept in local folder called ".secrets" + DB_ROOT_PWD: + file: .secrets/db_root_pwd.txt + MYSQL_PWD: + file: .secrets/mysql_pwd.txt + +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + # Public HTTP Port: + - '80:80' + # Public HTTPS Port: + - '443:443' + # Admin Web Port: + - '81:81' + environment: + # These are the settings to access your db + DB_MYSQL_HOST: "db" + DB_MYSQL_PORT: 3306 + DB_MYSQL_USER: "npm" + # DB_MYSQL_PASSWORD: "npm" # use secret instead + DB_MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD + DB_MYSQL_NAME: "npm" + # If you would rather use Sqlite, remove all DB_MYSQL_* lines above + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + secrets: + - MYSQL_PWD + depends_on: + - db + + db: + image: jc21/mariadb-aria + restart: unless-stopped + environment: + # MYSQL_ROOT_PASSWORD: "npm" # use secret instead + MYSQL_ROOT_PASSWORD__FILE: /run/secrets/DB_ROOT_PWD + MYSQL_DATABASE: "npm" + MYSQL_USER: "npm" + # MYSQL_PASSWORD: "npm" # use secret instead + MYSQL_PASSWORD__FILE: /run/secrets/MYSQL_PWD + MARIADB_AUTO_UPGRADE: '1' + volumes: + - ./mysql:/var/lib/mysql + secrets: + - DB_ROOT_PWD + - MYSQL_PWD +``` + + +## Disabling IPv6 + +On some Docker hosts IPv6 may not be enabled. In these cases, the following message may be seen in the log: + +> Address family not supported by protocol + +The easy fix is to add a Docker environment variable to the Nginx Proxy Manager stack: + +```yml + environment: + DISABLE_IPV6: 'true' +``` + +## Disabling IP Ranges Fetch + +By default, NPM fetches IP ranges from CloudFront and Cloudflare during application startup. In environments with limited internet access or to speed up container startup, this fetch can be disabled: + +```yml + environment: + IP_RANGES_FETCH_ENABLED: 'false' +``` + +## Custom Nginx Configurations + +If you are a more advanced user, you might be itching for extra Nginx customizability. + +NPM has the ability to include different custom configuration snippets in different places. + +You can add your custom configuration snippet files at `/data/nginx/custom` as follow: + + - `/data/nginx/custom/root_top.conf`: Included at the top of nginx.conf + - `/data/nginx/custom/root.conf`: Included at the very end of nginx.conf + - `/data/nginx/custom/http_top.conf`: Included at the top of the main http block + - `/data/nginx/custom/http.conf`: Included at the end of the main http block + - `/data/nginx/custom/events.conf`: Included at the end of the events block + - `/data/nginx/custom/stream.conf`: Included at the end of the main stream block + - `/data/nginx/custom/server_proxy.conf`: Included at the end of every proxy server block + - `/data/nginx/custom/server_redirect.conf`: Included at the end of every redirection server block + - `/data/nginx/custom/server_stream.conf`: Included at the end of every stream server block + - `/data/nginx/custom/server_stream_tcp.conf`: Included at the end of every TCP stream server block + - `/data/nginx/custom/server_stream_udp.conf`: Included at the end of every UDP stream server block + - `/data/nginx/custom/server_dead.conf`: Included at the end of every 404 server block + +Every file is optional. + + +## X-FRAME-OPTIONS Header + +You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header +value by specifying it as a Docker environment variable. The default if not specified is `deny`. + +```yml + ... + environment: + X_FRAME_OPTIONS: "sameorigin" + ... +``` + +## Customising logrotate settings + +By default, NPM rotates the access- and error logs weekly and keeps 4 and 10 log files respectively. +Depending on the usage, this can lead to large log files, especially access logs. +You can customise the logrotate configuration through a mount (if your custom config is `logrotate.custom`): + +```yml + volumes: + ... + - ./logrotate.custom:/etc/logrotate.d/nginx-proxy-manager +``` + +For reference, the default configuration can be found [here](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/rootfs/etc/logrotate.d/nginx-proxy-manager). + +## Enabling the geoip2 module + +To enable the geoip2 module, you can create the custom configuration file `/data/nginx/custom/root_top.conf` and include the following snippet: + +``` +load_module /usr/lib/nginx/modules/ngx_http_geoip2_module.so; +load_module /usr/lib/nginx/modules/ngx_stream_geoip2_module.so; +``` diff --git a/docs/src/faq/index.md b/docs/src/faq/index.md new file mode 100644 index 000000000..ea375f47b --- /dev/null +++ b/docs/src/faq/index.md @@ -0,0 +1,26 @@ +--- +outline: deep +--- + +# FAQ + +## Do I have to use Docker? + +Yes, that's how this project is packaged. + +This makes it easier to support the project when we have control over the version of Nginx other packages +use by the project. + +## Can I run it on a Raspberry Pi? + +Yes! The docker image is multi-arch and is built for a variety of architectures. If yours is +[not listed](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) please open a +[GitHub issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). + +## I can't get my service to proxy properly? + +Your best bet is to ask the [Reddit community for support](https://www.reddit.com/r/nginxproxymanager/). There's safety in numbers. + +## When adding username and password access control to a proxy host, I can no longer login into the app. + +Having an Access Control List (ACL) with username and password requires the browser to always send this username and password in the `Authorization` header on each request. If your proxied app also requires authentication (like Nginx Proxy Manager itself), most likely the app will also use the `Authorization` header to transmit this information, as this is the standardized header meant for this kind of information. However having multiples of the same headers is not allowed in the [internet standard](https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2) and almost all apps do not support multiple values in the `Authorization` header. Hence one of the two logins will be broken. This can only be fixed by either removing one of the logins or by changing the app to use other non-standard headers for authorization. diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md new file mode 100644 index 000000000..fcf176fa9 --- /dev/null +++ b/docs/src/guide/index.md @@ -0,0 +1,125 @@ +--- +outline: deep +--- + +# Guide + +::: raw +

+ + + + + + +

+::: + +This project comes as a pre-built docker image that enables you to easily forward to your websites +running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. + +- [Quick Setup](#quick-setup) +- [Full Setup](/setup/) +- [Screenshots](/screenshots/) + +## Project Goal + +I created this project to fill a personal need to provide users with an easy way to accomplish reverse +proxying hosts with SSL termination and it had to be so easy that a monkey could do it. This goal hasn't changed. +While there might be advanced options they are optional and the project should be as simple as possible +so that the barrier for entry here is low. + +::: raw +Buy Me A Coffee +::: + +## Features + +- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/) +- 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 +- Advanced Nginx configuration available for super users +- User management, permissions and audit log + + +## Hosting your home network + +I won't go in to too much detail here but here are the basics for someone new to this self-hosted world. + +1. Your home router will have a Port Forwarding section somewhere. Log in and find it +2. Add port forwarding for port 80 and 443 to the server hosting this project +3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns) +4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services + +## Quick Setup + +1. Install Docker and Docker-Compose + +- [Docker Install documentation](https://docs.docker.com/get-docker/) +- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/) + +2. Create a docker-compose.yml file similar to this: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt +``` + +This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more. + +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 +``` + +Immediately after logging in with this default user you will be asked to modify your details and change your password. + + +## Contributing + +All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. + +CI is used in this project. All PR's must pass before being considered. After passing, +docker builds for PR's are available on dockerhub for manual verifications. + +Documentation within the `develop` branch is available for preview at +[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com) + + +### Contributors + +Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). + + +## Getting Support + +1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) +2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) +3. [Reddit](https://reddit.com/r/nginxproxymanager) diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..74a263bd0 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,32 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Nginx Proxy Manager" + tagline: Expose your services easily and securely + image: + src: /logo.svg + alt: NPM Logo + actions: + - theme: brand + text: Get Started + link: /guide/ + - theme: alt + text: GitHub + link: https://github.com/NginxProxyManager/nginx-proxy-manager + +features: + - title: Get Connected + details: Expose web services on your network · Free SSL with Let's Encrypt · Designed with security in mind · Perfect for home networks + - title: Proxy Hosts + details: Expose your private network Web services and get connected anywhere. + - title: Beautiful UI + details: Based on Tabler, the interface is a pleasure to use. Configuring a server has never been so fun. + - title: Free SSL + details: Built in Let’s Encrypt support allows you to secure your Web services at no cost to you. The certificates even renew themselves! + - title: Docker FTW + details: Built as a Docker Image, Nginx Proxy Manager only requires a database. + - title: Multiple Users + details: Configure other users to either view or manage their own hosts. Full access permissions are available. +--- diff --git a/docs/src/public/github.png b/docs/src/public/github.png new file mode 100644 index 000000000..1480f031b Binary files /dev/null and b/docs/src/public/github.png differ diff --git a/docs/src/public/icon.png b/docs/src/public/icon.png new file mode 100644 index 000000000..7155df742 Binary files /dev/null and b/docs/src/public/icon.png differ diff --git a/docs/src/public/logo.svg b/docs/src/public/logo.svg new file mode 100644 index 000000000..23fa24529 --- /dev/null +++ b/docs/src/public/logo.svg @@ -0,0 +1 @@ +logo \ No newline at end of file diff --git a/docs/src/public/robots.txt b/docs/src/public/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/docs/src/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/docs/src/public/screenshots/access-lists.png b/docs/src/public/screenshots/access-lists.png new file mode 100644 index 000000000..058c6012e Binary files /dev/null and b/docs/src/public/screenshots/access-lists.png differ diff --git a/docs/src/public/screenshots/audit-log.png b/docs/src/public/screenshots/audit-log.png new file mode 100644 index 000000000..82de4ad06 Binary files /dev/null and b/docs/src/public/screenshots/audit-log.png differ diff --git a/docs/src/public/screenshots/certificates.png b/docs/src/public/screenshots/certificates.png new file mode 100644 index 000000000..222c79d29 Binary files /dev/null and b/docs/src/public/screenshots/certificates.png differ diff --git a/docs/src/public/screenshots/custom-settings.png b/docs/src/public/screenshots/custom-settings.png new file mode 100644 index 000000000..6ea4d58f7 Binary files /dev/null and b/docs/src/public/screenshots/custom-settings.png differ diff --git a/docs/src/public/screenshots/dashboard.png b/docs/src/public/screenshots/dashboard.png new file mode 100644 index 000000000..fce2711af Binary files /dev/null and b/docs/src/public/screenshots/dashboard.png differ diff --git a/docs/src/public/screenshots/dead-hosts.png b/docs/src/public/screenshots/dead-hosts.png new file mode 100644 index 000000000..0c36de3f6 Binary files /dev/null and b/docs/src/public/screenshots/dead-hosts.png differ diff --git a/docs/src/public/screenshots/login.png b/docs/src/public/screenshots/login.png new file mode 100644 index 000000000..99b12e196 Binary files /dev/null and b/docs/src/public/screenshots/login.png differ diff --git a/docs/src/public/screenshots/permissions.png b/docs/src/public/screenshots/permissions.png new file mode 100644 index 000000000..317d02cad Binary files /dev/null and b/docs/src/public/screenshots/permissions.png differ diff --git a/docs/src/public/screenshots/proxy-hosts-add.png b/docs/src/public/screenshots/proxy-hosts-add.png new file mode 100644 index 000000000..e003c0b75 Binary files /dev/null and b/docs/src/public/screenshots/proxy-hosts-add.png differ diff --git a/docs/src/public/screenshots/proxy-hosts.png b/docs/src/public/screenshots/proxy-hosts.png new file mode 100644 index 000000000..7485c0f6d Binary files /dev/null and b/docs/src/public/screenshots/proxy-hosts.png differ diff --git a/docs/src/public/screenshots/redirection-hosts.png b/docs/src/public/screenshots/redirection-hosts.png new file mode 100644 index 000000000..5b61b57a8 Binary files /dev/null and b/docs/src/public/screenshots/redirection-hosts.png differ diff --git a/docs/src/screenshots/index.md b/docs/src/screenshots/index.md new file mode 100644 index 000000000..8bd210285 --- /dev/null +++ b/docs/src/screenshots/index.md @@ -0,0 +1,20 @@ +--- +outline: deep +--- + +# Screenshots + +::: raw +
+ Login + Dashboard + Proxy Hosts + Add Proxy Host + Redirection Hosts + 404 Hosts + User Permissions + Certificates + Audit Log + Custom Settings +
+::: diff --git a/docs/src/setup/index.md b/docs/src/setup/index.md new file mode 100644 index 000000000..c2296da7f --- /dev/null +++ b/docs/src/setup/index.md @@ -0,0 +1,193 @@ +--- +outline: deep +--- + +# Full Setup Instructions + +## Running the App + +Create a `docker-compose.yml` file: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + + #environment: + # Uncomment this if you want to change the location of + # the SQLite DB file within the container + # DB_SQLITE_FILE: "/data/database.sqlite" + + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt +``` + +Then: + +```bash +docker compose up -d +``` + +## Using MySQL / MariaDB Database + +If you opt for the MySQL configuration you will have to provide the database server yourself. You can also use MariaDB. Here are the minimum supported versions: + +- MySQL v5.7.8+ +- MariaDB v10.2.7+ + +It's easy to use another docker container for your database also and link it as part of the docker stack, so that's what the following examples +are going to use. + +Here is an example of what your `docker-compose.yml` will look like when using a MariaDB container: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + environment: + # Mysql/Maria connection parameters: + DB_MYSQL_HOST: "db" + DB_MYSQL_PORT: 3306 + DB_MYSQL_USER: "npm" + DB_MYSQL_PASSWORD: "npm" + DB_MYSQL_NAME: "npm" + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + depends_on: + - db + + db: + image: 'jc21/mariadb-aria:latest' + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: 'npm' + MYSQL_DATABASE: 'npm' + MYSQL_USER: 'npm' + MYSQL_PASSWORD: 'npm' + MARIADB_AUTO_UPGRADE: '1' + volumes: + - ./mysql:/var/lib/mysql +``` + +::: warning + +Please note, that `DB_MYSQL_*` environment variables will take precedent over `DB_SQLITE_*` variables. So if you keep the MySQL variables, you will not be able to use SQLite. + +::: + +## Using Postgres database + +Similar to the MySQL server setup: + +```yml +services: + app: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + # These ports are in format : + - '80:80' # Public HTTP Port + - '443:443' # Public HTTPS Port + - '81:81' # Admin Web Port + # Add any other Stream port you want to expose + # - '21:21' # FTP + environment: + # Postgres parameters: + DB_POSTGRES_HOST: 'db' + DB_POSTGRES_PORT: '5432' + DB_POSTGRES_USER: 'npm' + DB_POSTGRES_PASSWORD: 'npmpass' + DB_POSTGRES_NAME: 'npm' + # Uncomment this if IPv6 is not enabled on your host + # DISABLE_IPV6: 'true' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt + depends_on: + - db + + db: + image: postgres:latest + environment: + POSTGRES_USER: 'npm' + POSTGRES_PASSWORD: 'npmpass' + POSTGRES_DB: 'npm' + volumes: + - ./postgres:/var/lib/postgresql/data +``` + +::: warning + +Custom Postgres schema is not supported, as such `public` will be used. + +::: + +## Running on Raspberry PI / ARM devices + +The docker images support the following architectures: +- amd64 +- arm64 +- armv7 + +The docker images are a manifest of all the architecture docker builds supported, so this means +you don't have to worry about doing anything special and you can follow the common instructions above. + +Check out the [dockerhub tags](https://hub.docker.com/r/jc21/nginx-proxy-manager/tags) +for a list of supported architectures and if you want one that doesn't exist, +[create a feature request](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=). + +Also, if you don't know how to already, follow [this guide to install docker and docker-compose](https://manre-universe.net/how-to-run-docker-and-docker-compose-on-raspbian/) +on Raspbian. + +Please note that the `jc21/mariadb-aria:latest` image might have some problems on some ARM devices, if you want a separate database container, use the `yobasystems/alpine-mariadb:latest` image. + +## Initial Run + +After the app is running for the first time, the following will happen: + +1. JWT keys will be generated and saved in the data folder +2. The database will initialize with table structures +3. A default admin user will be created + +This process can take a couple of minutes depending on your machine. + +## Default Administrator User + +``` +Email: admin@example.com +Password: changeme +``` + +Immediately after logging in with this default user you will be asked to modify your details and change your password. You can change defaults with: + + +``` + environment: + INITIAL_ADMIN_EMAIL: my@example.com + INITIAL_ADMIN_PASSWORD: mypassword1 +``` + + diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md new file mode 100644 index 000000000..cd54b45b9 --- /dev/null +++ b/docs/src/third-party/index.md @@ -0,0 +1,20 @@ +--- +outline: deep +--- + +# Third Party + +As this software gains popularity it's common to see it integrated with other platforms. Please be aware that unless specifically mentioned in the documentation of those +integrations, they are *not supported* by me. + +Known integrations: + +- [HomeAssistant Hass.io plugin](https://github.com/hassio-addons/addon-nginx-proxy-manager) +- [UnRaid / Synology](https://github.com/jlesage/docker-nginx-proxy-manager) +- [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) +- [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) +- [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) + + +If you would like your integration of NPM listed, please open a +[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) diff --git a/docs/src/upgrading/index.md b/docs/src/upgrading/index.md new file mode 100644 index 000000000..21076d19e --- /dev/null +++ b/docs/src/upgrading/index.md @@ -0,0 +1,16 @@ +--- +outline: deep +--- + +# Upgrading + +```bash +docker compose pull +docker compose up -d +``` + +This project will automatically update any databases or other requirements so you don't have to follow +any crazy instructions. These steps above will pull the latest updates and recreate the docker +containers. + +See the [list of releases](https://github.com/NginxProxyManager/nginx-proxy-manager/releases) for any upgrade steps specific to each release. diff --git a/docs/yarn.lock b/docs/yarn.lock new file mode 100644 index 000000000..c95905f68 --- /dev/null +++ b/docs/yarn.lock @@ -0,0 +1,1120 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/autocomplete-core@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" + integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-plugin-algolia-insights@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" + integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== + dependencies: + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-preset-algolia@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" + integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== + dependencies: + "@algolia/autocomplete-shared" "1.9.3" + +"@algolia/autocomplete-shared@1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" + integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== + +"@algolia/cache-browser-local-storage@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz#0cc26b96085e1115dac5fcb9d826651ba57faabc" + integrity sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg== + dependencies: + "@algolia/cache-common" "4.23.3" + +"@algolia/cache-common@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.23.3.tgz#3bec79092d512a96c9bfbdeec7cff4ad36367166" + integrity sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A== + +"@algolia/cache-in-memory@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz#3945f87cd21ffa2bec23890c85305b6b11192423" + integrity sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg== + dependencies: + "@algolia/cache-common" "4.23.3" + +"@algolia/client-account@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.23.3.tgz#8751bbf636e6741c95e7c778488dee3ee430ac6f" + integrity sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA== + dependencies: + "@algolia/client-common" "4.23.3" + "@algolia/client-search" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/client-analytics@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.23.3.tgz#f88710885278fe6fb6964384af59004a5a6f161d" + integrity sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA== + dependencies: + "@algolia/client-common" "4.23.3" + "@algolia/client-search" "4.23.3" + "@algolia/requester-common" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/client-common@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.23.3.tgz#891116aa0db75055a7ecc107649f7f0965774704" + integrity sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw== + dependencies: + "@algolia/requester-common" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/client-personalization@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.23.3.tgz#35fa8e5699b0295fbc400a8eb211dc711e5909db" + integrity sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g== + dependencies: + "@algolia/client-common" "4.23.3" + "@algolia/requester-common" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/client-search@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.23.3.tgz#a3486e6af13a231ec4ab43a915a1f318787b937f" + integrity sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw== + dependencies: + "@algolia/client-common" "4.23.3" + "@algolia/requester-common" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/logger-common@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.23.3.tgz#35c6d833cbf41e853a4f36ba37c6e5864920bfe9" + integrity sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g== + +"@algolia/logger-console@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.23.3.tgz#30f916781826c4db5f51fcd9a8a264a06e136985" + integrity sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A== + dependencies: + "@algolia/logger-common" "4.23.3" + +"@algolia/recommend@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.23.3.tgz#53d4f194d22d9c72dc05f3f7514c5878f87c5890" + integrity sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w== + dependencies: + "@algolia/cache-browser-local-storage" "4.23.3" + "@algolia/cache-common" "4.23.3" + "@algolia/cache-in-memory" "4.23.3" + "@algolia/client-common" "4.23.3" + "@algolia/client-search" "4.23.3" + "@algolia/logger-common" "4.23.3" + "@algolia/logger-console" "4.23.3" + "@algolia/requester-browser-xhr" "4.23.3" + "@algolia/requester-common" "4.23.3" + "@algolia/requester-node-http" "4.23.3" + "@algolia/transporter" "4.23.3" + +"@algolia/requester-browser-xhr@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz#9e47e76f60d540acc8b27b4ebc7a80d1b41938b9" + integrity sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw== + dependencies: + "@algolia/requester-common" "4.23.3" + +"@algolia/requester-common@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.23.3.tgz#7dbae896e41adfaaf1d1fa5f317f83a99afb04b3" + integrity sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw== + +"@algolia/requester-node-http@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz#c9f94a5cb96a15f48cea338ab6ef16bbd0ff989f" + integrity sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA== + dependencies: + "@algolia/requester-common" "4.23.3" + +"@algolia/transporter@4.23.3": + version "4.23.3" + resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.23.3.tgz#545b045b67db3850ddf0bbecbc6c84ff1f3398b7" + integrity sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ== + dependencies: + "@algolia/cache-common" "4.23.3" + "@algolia/logger-common" "4.23.3" + "@algolia/requester-common" "4.23.3" + +"@babel/helper-string-parser@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" + integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== + +"@babel/helper-validator-identifier@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" + integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== + +"@babel/parser@^7.25.3": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.8.tgz#f6aaf38e80c36129460c1657c0762db584c9d5e2" + integrity sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ== + dependencies: + "@babel/types" "^7.25.8" + +"@babel/types@^7.25.8": + version "7.25.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" + integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== + dependencies: + "@babel/helper-string-parser" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + to-fast-properties "^2.0.0" + +"@docsearch/css@3.6.2", "@docsearch/css@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.2.tgz#ccd9c83dbfeaf34efe4e3547ee596714ae7e5891" + integrity sha512-vKNZepO2j7MrYBTZIGXvlUOIR+v9KRf70FApRgovWrj3GTs1EITz/Xb0AOlm1xsQBp16clVZj1SY/qaOJbQtZw== + +"@docsearch/js@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.6.2.tgz#000d7d255e8387e7c5b82c7b87d3060398e1605d" + integrity sha512-pS4YZF+VzUogYrkblCucQ0Oy2m8Wggk8Kk7lECmZM60hTbaydSIhJTTiCrmoxtBqV8wxORnOqcqqOfbmkkQEcA== + dependencies: + "@docsearch/react" "3.6.2" + preact "^10.0.0" + +"@docsearch/react@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.2.tgz#32b16dd7d5614f0d39e6bc018549816b68d171b8" + integrity sha512-rtZce46OOkVflCQH71IdbXSFK+S8iJZlUF56XBW5rIgx/eG5qoomC7Ag3anZson1bBac/JFQn7XOBfved/IMRA== + dependencies: + "@algolia/autocomplete-core" "1.9.3" + "@algolia/autocomplete-preset-algolia" "1.9.3" + "@docsearch/css" "3.6.2" + algoliasearch "^4.19.1" + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@rollup/rollup-android-arm-eabi@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54" + integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA== + +"@rollup/rollup-android-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e" + integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA== + +"@rollup/rollup-darwin-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f" + integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== + +"@rollup/rollup-darwin-x64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724" + integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb" + integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA== + +"@rollup/rollup-linux-arm-musleabihf@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3" + integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw== + +"@rollup/rollup-linux-arm64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496" + integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA== + +"@rollup/rollup-linux-arm64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065" + integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d" + integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw== + +"@rollup/rollup-linux-riscv64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983" + integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg== + +"@rollup/rollup-linux-s390x-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58" + integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g== + +"@rollup/rollup-linux-x64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b" + integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A== + +"@rollup/rollup-linux-x64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127" + integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ== + +"@rollup/rollup-win32-arm64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5" + integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ== + +"@rollup/rollup-win32-ia32-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2" + integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ== + +"@rollup/rollup-win32-x64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818" + integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw== + +"@shikijs/core@1.22.0", "@shikijs/core@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.22.0.tgz#74e5d4485e5f7afa85109e322b42e400686f92bb" + integrity sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q== + dependencies: + "@shikijs/engine-javascript" "1.22.0" + "@shikijs/engine-oniguruma" "1.22.0" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + hast-util-to-html "^9.0.3" + +"@shikijs/engine-javascript@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz#2e5db29f0421755492f5279f8224ef7a7f907a29" + integrity sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw== + dependencies: + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" + oniguruma-to-js "0.4.3" + +"@shikijs/engine-oniguruma@1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz#74c661fac4cd1f08f2c09b5d6e2fd2a6720d0401" + integrity sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw== + dependencies: + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" + +"@shikijs/transformers@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-1.22.0.tgz#f36fa4d769e36db9a91e09877cf48b3a04d26aba" + integrity sha512-k7iMOYuGQA62KwAuJOQBgH2IQb5vP8uiB3lMvAMGUgAMMurePOx3Z7oNqJdcpxqZP6I9cc7nc4DNqSKduCxmdg== + dependencies: + shiki "1.22.0" + +"@shikijs/types@1.22.0", "@shikijs/types@^1.22.0": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.22.0.tgz#d2a572381395c9308b472c8199b8e0289753b9ad" + integrity sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww== + dependencies: + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + +"@shikijs/vscode-textmate@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab" + integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA== + +"@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/hast@^3.0.0", "@types/hast@^3.0.4": + 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: + "@types/unist" "*" + +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/web-bluetooth@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" + integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + +"@ungap/structured-clone@^1.0.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== + +"@vitejs/plugin-vue@^5.1.4": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz#72b8b705cfce36b00b59af196195146e356500c4" + integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A== + +"@vue/compiler-core@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.11.tgz#3dcd0c1bab10732f44ab1790735afb03a4b69edc" + integrity sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.11" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.11.tgz#950f8fc610e26326fed008b8d102cc8ee78a6ce5" + integrity sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew== + dependencies: + "@vue/compiler-core" "3.5.11" + "@vue/shared" "3.5.11" + +"@vue/compiler-sfc@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.11.tgz#68ba7bc6fed4fec6892aed118cb3ee8e4b180d06" + integrity sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.11" + "@vue/compiler-dom" "3.5.11" + "@vue/compiler-ssr" "3.5.11" + "@vue/shared" "3.5.11" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.47" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.11.tgz#02d9891c7a649bbf06490ecd8d24dd1575d53e60" + integrity sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA== + dependencies: + "@vue/compiler-dom" "3.5.11" + "@vue/shared" "3.5.11" + +"@vue/devtools-api@^7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.4.6.tgz#5e9249d6de3cee58624e511fdc727837b1f2d273" + integrity sha512-XipBV5k0/IfTr0sNBDTg7OBUCp51cYMMXyPxLXJZ4K/wmUeMqt8cVdr2ZZGOFq+si/jTyCYnNxeKoyev5DOUUA== + dependencies: + "@vue/devtools-kit" "^7.4.6" + +"@vue/devtools-kit@^7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.4.6.tgz#80aa30db65bf5b2b0eda4e818749d3c37d80f709" + integrity sha512-NbYBwPWgEic1AOd9bWExz9weBzFdjiIfov0yRn4DrRfR+EQJCI9dn4I0XS7IxYGdkmUJi8mFW42LLk18WsGqew== + dependencies: + "@vue/devtools-shared" "^7.4.6" + birpc "^0.2.17" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" + superjson "^2.2.1" + +"@vue/devtools-shared@^7.4.6": + version "7.4.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.4.6.tgz#492c2301caacc83a32542dd95dfcae3980621417" + integrity sha512-rPeSBzElnHYMB05Cc056BQiJpgocQjY8XVulgni+O9a9Gr9tNXgPteSzFFD+fT/iWMxNuUgGKs9CuW5DZewfIg== + dependencies: + rfdc "^1.4.1" + +"@vue/reactivity@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.11.tgz#d27df4fba10c2de1c7234701f18247a775b7a391" + integrity sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w== + dependencies: + "@vue/shared" "3.5.11" + +"@vue/runtime-core@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.11.tgz#7beccd013efe5d33981ffd6b6e05d0a5b9058316" + integrity sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA== + dependencies: + "@vue/reactivity" "3.5.11" + "@vue/shared" "3.5.11" + +"@vue/runtime-dom@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.11.tgz#14a3181ab7057de41b345b4b3d37b744b3ff8ff5" + integrity sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ== + dependencies: + "@vue/reactivity" "3.5.11" + "@vue/runtime-core" "3.5.11" + "@vue/shared" "3.5.11" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.11.tgz#74f558371dfc39f3b0f26f95d089a1a4d1676027" + integrity sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA== + dependencies: + "@vue/compiler-ssr" "3.5.11" + "@vue/shared" "3.5.11" + +"@vue/shared@3.5.11", "@vue/shared@^3.5.11": + version "3.5.11" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.11.tgz#464b840afc89be9373addff9eeb9dfc98bf3fe2e" + integrity sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ== + +"@vueuse/core@11.1.0", "@vueuse/core@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-11.1.0.tgz#a104f33c899a15f3b28d3eb7b20738501a3a5035" + integrity sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg== + dependencies: + "@types/web-bluetooth" "^0.0.20" + "@vueuse/metadata" "11.1.0" + "@vueuse/shared" "11.1.0" + vue-demi ">=0.14.10" + +"@vueuse/integrations@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-11.1.0.tgz#1e2c1d43b2d389fc4b4d0a7ee08091665698b9ad" + integrity sha512-O2ZgrAGPy0qAjpoI2YR3egNgyEqwG85fxfwmA9BshRIGjV4G6yu6CfOPpMHAOoCD+UfsIl7Vb1bXJ6ifrHYDDA== + dependencies: + "@vueuse/core" "11.1.0" + "@vueuse/shared" "11.1.0" + vue-demi ">=0.14.10" + +"@vueuse/metadata@11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-11.1.0.tgz#ad367d2a51d985129724425923b3cf95f0faf27b" + integrity sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg== + +"@vueuse/shared@11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-11.1.0.tgz#3bfc3aa555c2a456c21945ec7f127d71938d12e8" + integrity sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w== + dependencies: + vue-demi ">=0.14.10" + +algoliasearch@^4.19.1: + version "4.23.3" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.23.3.tgz#e09011d0a3b0651444916a3e6bbcba064ec44b60" + integrity sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg== + dependencies: + "@algolia/cache-browser-local-storage" "4.23.3" + "@algolia/cache-common" "4.23.3" + "@algolia/cache-in-memory" "4.23.3" + "@algolia/client-account" "4.23.3" + "@algolia/client-analytics" "4.23.3" + "@algolia/client-common" "4.23.3" + "@algolia/client-personalization" "4.23.3" + "@algolia/client-search" "4.23.3" + "@algolia/logger-common" "4.23.3" + "@algolia/logger-console" "4.23.3" + "@algolia/recommend" "4.23.3" + "@algolia/requester-browser-xhr" "4.23.3" + "@algolia/requester-common" "4.23.3" + "@algolia/requester-node-http" "4.23.3" + "@algolia/transporter" "4.23.3" + +birpc@^0.2.17: + version "0.2.19" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-0.2.19.tgz#cdd183a4a70ba103127d49765b4a71349da5a0ca" + integrity sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ== + +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== + +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== + +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== + +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== + +copy-anything@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" + integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + dependencies: + is-what "^4.1.8" + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.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: + dequal "^2.0.0" + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +focus-trap@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.6.0.tgz#7f3edab8135eaca92ab59b6e963eb5cc42ded715" + integrity sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ== + dependencies: + tabbable "^6.2.0" + +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== + +hast-util-to-html@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz#a9999a0ba6b4919576a9105129fead85d37f302b" + integrity sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^3.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + stringify-entities "^4.0.0" + zwitch "^2.0.4" + +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: + "@types/hast" "^3.0.0" + +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + +magic-string@^0.30.11: + version "0.30.11" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" + integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +mark.js@8.11.1: + version "8.11.1" + resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + integrity sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ== + +mdast-util-to-hast@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" + integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== + 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" + +micromark-util-character@^2.0.0: + version "2.1.0" + 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" + +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-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: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +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== + +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== + +minisearch@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.1.0.tgz#f5830e9109b5919ee7b291c29a304f381aa68770" + integrity sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA== + +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== + +oniguruma-to-js@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740" + integrity sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ== + dependencies: + regex "^4.3.2" + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + +postcss@^8.4.43, postcss@^8.4.47: + version "8.4.47" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" + integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== + dependencies: + nanoid "^3.3.7" + picocolors "^1.1.0" + source-map-js "^1.2.1" + +preact@^10.0.0: + version "10.21.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.21.0.tgz#5b0335c873a1724deb66e517830db4fd310c24f6" + integrity sha512-aQAIxtzWEwH8ou+OovWVSVNlFImL7xUCwJX3YMqA3U8iKCNC34999fFOnWjYNsylgfPgMexpbk7WYOLtKr/mxg== + +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== + +regex@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/regex/-/regex-4.3.3.tgz#8cda73ccbdfa7c5691881d02f9bb142dba9daa6a" + integrity sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg== + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^4.20.0: + version "4.24.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05" + integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.24.0" + "@rollup/rollup-android-arm64" "4.24.0" + "@rollup/rollup-darwin-arm64" "4.24.0" + "@rollup/rollup-darwin-x64" "4.24.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.0" + "@rollup/rollup-linux-arm-musleabihf" "4.24.0" + "@rollup/rollup-linux-arm64-gnu" "4.24.0" + "@rollup/rollup-linux-arm64-musl" "4.24.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0" + "@rollup/rollup-linux-riscv64-gnu" "4.24.0" + "@rollup/rollup-linux-s390x-gnu" "4.24.0" + "@rollup/rollup-linux-x64-gnu" "4.24.0" + "@rollup/rollup-linux-x64-musl" "4.24.0" + "@rollup/rollup-win32-arm64-msvc" "4.24.0" + "@rollup/rollup-win32-ia32-msvc" "4.24.0" + "@rollup/rollup-win32-x64-msvc" "4.24.0" + fsevents "~2.3.2" + +shiki@1.22.0, shiki@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.22.0.tgz#45d1dfff0e03a598af70e2ec8592f14ef07827b4" + integrity sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw== + dependencies: + "@shikijs/core" "1.22.0" + "@shikijs/engine-javascript" "1.22.0" + "@shikijs/engine-oniguruma" "1.22.0" + "@shikijs/types" "1.22.0" + "@shikijs/vscode-textmate" "^9.3.0" + "@types/hast" "^3.0.4" + +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== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +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== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + +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: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +superjson@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.1.tgz#9377a7fa80fedb10c851c9dbffd942d4bcf79733" + integrity sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA== + dependencies: + copy-anything "^3.0.2" + +tabbable@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + +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 sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +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: + "@types/unist" "^3.0.0" + +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: + "@types/unist" "^3.0.0" + +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: + "@types/unist" "^3.0.0" + +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: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +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: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +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: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite@^5.4.8: + version "5.4.19" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959" + integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vitepress@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.4.0.tgz#5e879230d98e5c4e5aec91daade6945bbc18934b" + integrity sha512-JXCv4EsKTDyAFb6C/UjZr7nsGAzZ6mafVk2rx7rG5o8N+B/4QstIk+iEOe/9dKoU6V624UIC6g1pZ+K63rxhlw== + dependencies: + "@docsearch/css" "^3.6.2" + "@docsearch/js" "^3.6.2" + "@shikijs/core" "^1.22.0" + "@shikijs/transformers" "^1.22.0" + "@shikijs/types" "^1.22.0" + "@types/markdown-it" "^14.1.2" + "@vitejs/plugin-vue" "^5.1.4" + "@vue/devtools-api" "^7.4.6" + "@vue/shared" "^3.5.11" + "@vueuse/core" "^11.1.0" + "@vueuse/integrations" "^11.1.0" + focus-trap "^7.6.0" + mark.js "8.11.1" + minisearch "^7.1.0" + shiki "^1.22.0" + vite "^5.4.8" + vue "^3.5.11" + +vue-demi@>=0.14.10: + version "0.14.10" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04" + integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg== + +vue@^3.5.11: + version "3.5.11" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.11.tgz#3e307183797629f701e303a0a008f517ae031483" + integrity sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg== + dependencies: + "@vue/compiler-dom" "3.5.11" + "@vue/compiler-sfc" "3.5.11" + "@vue/runtime-dom" "3.5.11" + "@vue/server-renderer" "3.5.11" + "@vue/shared" "3.5.11" + +zwitch@^2.0.4: + 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/frontend/.babelrc b/frontend/.babelrc new file mode 100644 index 000000000..54071ecd1 --- /dev/null +++ b/frontend/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + [ + "env", + { + "targets": { + "browsers": [ + "Chrome >= 65" + ] + }, + "debug": false, + "modules": false, + "useBuiltIns": "usage" + } + ] + ] +} \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..c8f4b4f98 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +webpack_stats.html +yarn-error.log diff --git a/src/frontend/app-images/default-avatar.jpg b/frontend/app-images/default-avatar.jpg similarity index 100% rename from src/frontend/app-images/default-avatar.jpg rename to frontend/app-images/default-avatar.jpg diff --git a/frontend/app-images/favicons/android-chrome-192x192.png b/frontend/app-images/favicons/android-chrome-192x192.png new file mode 100644 index 000000000..7155df742 Binary files /dev/null and b/frontend/app-images/favicons/android-chrome-192x192.png differ diff --git a/frontend/app-images/favicons/android-chrome-512x512.png b/frontend/app-images/favicons/android-chrome-512x512.png new file mode 100644 index 000000000..8f95e0031 Binary files /dev/null and b/frontend/app-images/favicons/android-chrome-512x512.png differ diff --git a/frontend/app-images/favicons/apple-touch-icon.png b/frontend/app-images/favicons/apple-touch-icon.png new file mode 100644 index 000000000..e8f243a69 Binary files /dev/null and b/frontend/app-images/favicons/apple-touch-icon.png differ diff --git a/src/frontend/app-images/favicons/browserconfig.xml b/frontend/app-images/favicons/browserconfig.xml similarity index 54% rename from src/frontend/app-images/favicons/browserconfig.xml rename to frontend/app-images/favicons/browserconfig.xml index e781b8870..87c8dbd2f 100644 --- a/src/frontend/app-images/favicons/browserconfig.xml +++ b/frontend/app-images/favicons/browserconfig.xml @@ -2,8 +2,8 @@ - - #f0ad00 + + #333333 diff --git a/frontend/app-images/favicons/favicon-16x16.png b/frontend/app-images/favicons/favicon-16x16.png new file mode 100644 index 000000000..e8a183ee8 Binary files /dev/null and b/frontend/app-images/favicons/favicon-16x16.png differ diff --git a/frontend/app-images/favicons/favicon-32x32.png b/frontend/app-images/favicons/favicon-32x32.png new file mode 100644 index 000000000..4fa0cf0b8 Binary files /dev/null and b/frontend/app-images/favicons/favicon-32x32.png differ diff --git a/frontend/app-images/favicons/favicon.ico b/frontend/app-images/favicons/favicon.ico new file mode 100644 index 000000000..9eae8b5f6 Binary files /dev/null and b/frontend/app-images/favicons/favicon.ico differ diff --git a/frontend/app-images/favicons/mstile-150x150.png b/frontend/app-images/favicons/mstile-150x150.png new file mode 100644 index 000000000..4a3fd8c17 Binary files /dev/null and b/frontend/app-images/favicons/mstile-150x150.png differ diff --git a/frontend/app-images/favicons/safari-pinned-tab.svg b/frontend/app-images/favicons/safari-pinned-tab.svg new file mode 100644 index 000000000..5f9ea3f6e --- /dev/null +++ b/frontend/app-images/favicons/safari-pinned-tab.svg @@ -0,0 +1,85 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + diff --git a/src/frontend/app-images/favicons/manifest.json b/frontend/app-images/favicons/site.webmanifest similarity index 65% rename from src/frontend/app-images/favicons/manifest.json rename to frontend/app-images/favicons/site.webmanifest index c0a1e1612..99d1016eb 100644 --- a/src/frontend/app-images/favicons/manifest.json +++ b/frontend/app-images/favicons/site.webmanifest @@ -1,13 +1,14 @@ { "name": "", + "short_name": "", "icons": [ { - "src": "/images/favicon/android-chrome-192x192.png", + "src": "/images/favicons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/images/favicon/android-chrome-512x512.png", + "src": "/images/favicons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } @@ -15,4 +16,4 @@ "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" -} \ No newline at end of file +} diff --git a/frontend/app-images/logo-256.png b/frontend/app-images/logo-256.png new file mode 100644 index 000000000..2bfb661dc Binary files /dev/null and b/frontend/app-images/logo-256.png differ diff --git a/frontend/app-images/logo-text-vertical-grey.png b/frontend/app-images/logo-text-vertical-grey.png new file mode 100644 index 000000000..70273171a Binary files /dev/null and b/frontend/app-images/logo-text-vertical-grey.png differ diff --git a/frontend/fonts/feather b/frontend/fonts/feather new file mode 120000 index 000000000..440203ba2 --- /dev/null +++ b/frontend/fonts/feather @@ -0,0 +1 @@ +../node_modules/tabler-ui/dist/assets/fonts/feather \ No newline at end of file diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff new file mode 100644 index 000000000..96d8768ea Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 new file mode 100644 index 000000000..e97a22181 Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700.woff2 differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff new file mode 100644 index 000000000..0829caef1 Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 new file mode 100644 index 000000000..7c901cd84 Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-700italic.woff2 differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff new file mode 100644 index 000000000..99652481a Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 new file mode 100644 index 000000000..343e5ba8d Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-italic.woff2 differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff new file mode 100644 index 000000000..92c3260ef Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff differ diff --git a/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 new file mode 100644 index 000000000..d552543be Binary files /dev/null and b/frontend/fonts/source-sans-pro/source-sans-pro-v14-latin-ext_latin-regular.woff2 differ diff --git a/src/backend/views/index.ejs b/frontend/html/index.ejs similarity index 87% rename from src/backend/views/index.ejs rename to frontend/html/index.ejs index a8b8da378..ae08b012e 100644 --- a/src/backend/views/index.ejs +++ b/frontend/html/index.ejs @@ -2,7 +2,7 @@ <%- include partials/header.ejs %>
- +
diff --git a/src/backend/views/login.ejs b/frontend/html/login.ejs similarity index 89% rename from src/backend/views/login.ejs rename to frontend/html/login.ejs index 18683b0b1..bc4b9a27f 100644 --- a/src/backend/views/login.ejs +++ b/frontend/html/login.ejs @@ -2,7 +2,7 @@ <%- include partials/header.ejs %>
- +
diff --git a/frontend/html/partials/footer.ejs b/frontend/html/partials/footer.ejs new file mode 100644 index 000000000..7fb2bd61b --- /dev/null +++ b/frontend/html/partials/footer.ejs @@ -0,0 +1,2 @@ + + diff --git a/frontend/html/partials/header.ejs b/frontend/html/partials/header.ejs new file mode 100644 index 000000000..cabb9df28 --- /dev/null +++ b/frontend/html/partials/header.ejs @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + <%- title %> + + + + + + + + + + + + + + diff --git a/frontend/images b/frontend/images new file mode 120000 index 000000000..37c318540 --- /dev/null +++ b/frontend/images @@ -0,0 +1 @@ +./node_modules/tabler-ui/dist/assets/images \ No newline at end of file diff --git a/src/frontend/js/app/api.js b/frontend/js/app/api.js similarity index 87% rename from src/frontend/js/app/api.js rename to frontend/js/app/api.js index 74356f06d..6e33a6dca 100644 --- a/src/frontend/js/app/api.js +++ b/frontend/js/app/api.js @@ -53,7 +53,7 @@ function fetch(verb, path, data, options) { contentType: options.contentType || 'application/json; charset=UTF-8', processData: options.processData || true, crossDomain: true, - timeout: options.timeout ? options.timeout : 30000, + timeout: options.timeout ? options.timeout : 180000, xhrFields: { withCredentials: true }, @@ -139,7 +139,11 @@ function FileUpload(path, fd) { xhr.onreadystatechange = function () { if (this.readyState === XMLHttpRequest.DONE) { if (xhr.status !== 200 && xhr.status !== 201) { - reject(new Error('Upload failed: ' + xhr.status)); + try { + reject(new Error('Upload failed: ' + JSON.parse(xhr.responseText).error.message)); + } catch (err) { + reject(new Error('Upload failed: ' + xhr.status)); + } } else { resolve(xhr.responseText); } @@ -148,6 +152,51 @@ function FileUpload(path, fd) { }); } +//ref : https://codepen.io/chrisdpratt/pen/RKxJNo +function DownloadFile(verb, path, filename) { + return new Promise(function (resolve, reject) { + let api_url = '/api/'; + let url = api_url + path; + let token = Tokens.getTopToken(); + + $.ajax({ + url: url, + type: verb, + crossDomain: true, + xhrFields: { + withCredentials: true, + responseType: 'blob' + }, + + beforeSend: function (xhr) { + xhr.setRequestHeader('Authorization', 'Bearer ' + (token ? token.t : null)); + }, + + success: function (data) { + var a = document.createElement('a'); + var url = window.URL.createObjectURL(data); + a.href = url; + a.download = filename; + document.body.append(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }, + + error: function (xhr, status, error_thrown) { + let code = 400; + + if (typeof xhr.responseJSON !== 'undefined' && typeof xhr.responseJSON.error !== 'undefined' && typeof xhr.responseJSON.error.message !== 'undefined') { + error_thrown = xhr.responseJSON.error.message; + code = xhr.responseJSON.error.code || 500; + } + + reject(new ApiError(error_thrown, xhr.responseText, code)); + } + }); + }); +} + module.exports = { status: function () { return fetch('get', ''); @@ -587,7 +636,9 @@ module.exports = { * @param {Object} data */ create: function (data) { - return fetch('post', 'nginx/certificates', data); + + const timeout = 180000 + (data && data.meta && data.meta.propagation_seconds ? Number(data.meta.propagation_seconds) * 1000 : 0); + return fetch('post', 'nginx/certificates', data, {timeout}); }, /** @@ -630,8 +681,26 @@ module.exports = { * @param {Number} id * @returns {Promise} */ - renew: function (id) { - return fetch('post', 'nginx/certificates/' + id + '/renew'); + renew: function (id, timeout = 180000) { + return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout}); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + testHttpChallenge: function (domains) { + return fetch('get', 'nginx/certificates/test-http?' + new URLSearchParams({ + domains: JSON.stringify(domains), + })); + }, + + /** + * @param {Number} id + * @returns {Promise} + */ + download: function (id) { + return DownloadFile('get', "nginx/certificates/" + id + "/download", "certificate.zip") } } }, diff --git a/src/frontend/js/app/audit-log/list/item.ejs b/frontend/js/app/audit-log/list/item.ejs similarity index 100% rename from src/frontend/js/app/audit-log/list/item.ejs rename to frontend/js/app/audit-log/list/item.ejs diff --git a/src/frontend/js/app/audit-log/list/item.js b/frontend/js/app/audit-log/list/item.js similarity index 100% rename from src/frontend/js/app/audit-log/list/item.js rename to frontend/js/app/audit-log/list/item.js diff --git a/src/frontend/js/app/audit-log/list/main.ejs b/frontend/js/app/audit-log/list/main.ejs similarity index 100% rename from src/frontend/js/app/audit-log/list/main.ejs rename to frontend/js/app/audit-log/list/main.ejs diff --git a/src/frontend/js/app/audit-log/list/main.js b/frontend/js/app/audit-log/list/main.js similarity index 86% rename from src/frontend/js/app/audit-log/list/main.js rename to frontend/js/app/audit-log/list/main.js index 88e639e80..9d3e26fb1 100644 --- a/src/frontend/js/app/audit-log/list/main.js +++ b/frontend/js/app/audit-log/list/main.js @@ -9,7 +9,7 @@ const TableBody = Mn.CollectionView.extend({ module.exports = Mn.View.extend({ tagName: 'table', - className: 'table table-hover table-outline table-vcenter text-nowrap card-table', + className: 'table table-hover table-outline table-vcenter card-table', template: template, regions: { diff --git a/frontend/js/app/audit-log/main.ejs b/frontend/js/app/audit-log/main.ejs new file mode 100644 index 000000000..8d182b595 --- /dev/null +++ b/frontend/js/app/audit-log/main.ejs @@ -0,0 +1,25 @@ +
+
+
+

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

+
+ +
+
+
+
+
+
+ +
+
+ +
+
diff --git a/frontend/js/app/audit-log/main.js b/frontend/js/app/audit-log/main.js new file mode 100644 index 000000000..0d03c5ca8 --- /dev/null +++ b/frontend/js/app/audit-log/main.js @@ -0,0 +1,82 @@ +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/src/frontend/js/app/audit-log/meta.ejs b/frontend/js/app/audit-log/meta.ejs similarity index 100% rename from src/frontend/js/app/audit-log/meta.ejs rename to frontend/js/app/audit-log/meta.ejs diff --git a/src/frontend/js/app/audit-log/meta.js b/frontend/js/app/audit-log/meta.js similarity index 100% rename from src/frontend/js/app/audit-log/meta.js rename to frontend/js/app/audit-log/meta.js diff --git a/src/frontend/js/app/cache.js b/frontend/js/app/cache.js similarity index 100% rename from src/frontend/js/app/cache.js rename to frontend/js/app/cache.js diff --git a/frontend/js/app/controller.js b/frontend/js/app/controller.js new file mode 100644 index 000000000..ebddd7807 --- /dev/null +++ b/frontend/js/app/controller.js @@ -0,0 +1,441 @@ +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 () { + const 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 () { + const 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')) { + const 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')) { + const 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')) { + const 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')) { + const 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')) { + const 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')) { + const 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 () { + const 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 () { + const 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/src/frontend/js/app/dashboard/main.ejs b/frontend/js/app/dashboard/main.ejs similarity index 100% rename from src/frontend/js/app/dashboard/main.ejs rename to frontend/js/app/dashboard/main.ejs diff --git a/frontend/js/app/dashboard/main.js b/frontend/js/app/dashboard/main.js new file mode 100644 index 000000000..ba4a99a67 --- /dev/null +++ b/frontend/js/app/dashboard/main.js @@ -0,0 +1,90 @@ +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 () { + const 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 () { + const 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 + const 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/src/frontend/js/app/empty/main.ejs b/frontend/js/app/empty/main.ejs similarity index 100% rename from src/frontend/js/app/empty/main.ejs rename to frontend/js/app/empty/main.ejs diff --git a/src/frontend/js/app/empty/main.js b/frontend/js/app/empty/main.js similarity index 100% rename from src/frontend/js/app/empty/main.js rename to frontend/js/app/empty/main.js diff --git a/src/frontend/js/app/error/main.ejs b/frontend/js/app/error/main.ejs similarity index 100% rename from src/frontend/js/app/error/main.ejs rename to frontend/js/app/error/main.ejs diff --git a/src/frontend/js/app/error/main.js b/frontend/js/app/error/main.js similarity index 100% rename from src/frontend/js/app/error/main.js rename to frontend/js/app/error/main.js diff --git a/src/frontend/js/app/help/main.ejs b/frontend/js/app/help/main.ejs similarity index 100% rename from src/frontend/js/app/help/main.ejs rename to frontend/js/app/help/main.ejs diff --git a/src/frontend/js/app/help/main.js b/frontend/js/app/help/main.js similarity index 100% rename from src/frontend/js/app/help/main.js rename to frontend/js/app/help/main.js diff --git a/src/frontend/js/app/i18n.js b/frontend/js/app/i18n.js similarity index 100% rename from src/frontend/js/app/i18n.js rename to frontend/js/app/i18n.js diff --git a/src/frontend/js/app/main.js b/frontend/js/app/main.js similarity index 100% rename from src/frontend/js/app/main.js rename to frontend/js/app/main.js diff --git a/src/frontend/js/app/nginx/access/delete.ejs b/frontend/js/app/nginx/access/delete.ejs similarity index 100% rename from src/frontend/js/app/nginx/access/delete.ejs rename to frontend/js/app/nginx/access/delete.ejs diff --git a/src/frontend/js/app/nginx/access/delete.js b/frontend/js/app/nginx/access/delete.js similarity index 100% rename from src/frontend/js/app/nginx/access/delete.js rename to frontend/js/app/nginx/access/delete.js diff --git a/frontend/js/app/nginx/access/form.ejs b/frontend/js/app/nginx/access/form.ejs new file mode 100644 index 000000000..79220b14b --- /dev/null +++ b/frontend/js/app/nginx/access/form.ejs @@ -0,0 +1,108 @@ + diff --git a/src/frontend/js/app/nginx/access/form.js b/frontend/js/app/nginx/access/form.js similarity index 50% rename from src/frontend/js/app/nginx/access/form.js rename to frontend/js/app/nginx/access/form.js index d4e77c905..bb0755481 100644 --- a/src/frontend/js/app/nginx/access/form.js +++ b/frontend/js/app/nginx/access/form.js @@ -3,6 +3,7 @@ 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'); @@ -10,20 +11,28 @@ 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', - form: 'form', - buttons: '.modal-footer button', - cancel: 'button.cancel', - save: 'button.save' + 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' + items_region: '@ui.items_region', + clients_region: '@ui.clients_region' }, events: { @@ -35,9 +44,10 @@ module.exports = Mn.View.extend({ return; } - let view = this; - let form_data = this.ui.form.serializeJSON(); - let items_data = []; + 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) { @@ -48,16 +58,30 @@ module.exports = Mn.View.extend({ } }); - if (!items_data.length) { - alert('You must specify at least 1 Username and Password combination'); + 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, - items: items_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; @@ -83,23 +107,42 @@ module.exports = Mn.View.extend({ 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'); - // Add empty items to the end of the list. This is cheating but hey I don't have the time to do it right - let items_to_add = 5 - items.length; - if (items_to_add) { - for (let i = 0; i < items_to_add; i++) { - items.push({}); - } - } + // 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) { diff --git a/frontend/js/app/nginx/access/form/client.ejs b/frontend/js/app/nginx/access/form/client.ejs new file mode 100644 index 000000000..6b767b83f --- /dev/null +++ b/frontend/js/app/nginx/access/form/client.ejs @@ -0,0 +1,13 @@ +
+
+ +
+
+
+
+ +
+
diff --git a/frontend/js/app/nginx/access/form/client.js b/frontend/js/app/nginx/access/form/client.js new file mode 100644 index 000000000..b4c00e2e3 --- /dev/null +++ b/frontend/js/app/nginx/access/form/client.js @@ -0,0 +1,7 @@ +const Mn = require('backbone.marionette'); +const template = require('./client.ejs'); + +module.exports = Mn.View.extend({ + template: template, + className: 'row' +}); diff --git a/src/frontend/js/app/nginx/access/form/item.ejs b/frontend/js/app/nginx/access/form/item.ejs similarity index 100% rename from src/frontend/js/app/nginx/access/form/item.ejs rename to frontend/js/app/nginx/access/form/item.ejs diff --git a/src/frontend/js/app/nginx/access/form/item.js b/frontend/js/app/nginx/access/form/item.js similarity index 100% rename from src/frontend/js/app/nginx/access/form/item.js rename to frontend/js/app/nginx/access/form/item.js diff --git a/src/frontend/js/app/nginx/access/list/item.ejs b/frontend/js/app/nginx/access/list/item.ejs similarity index 65% rename from src/frontend/js/app/nginx/access/list/item.ejs rename to frontend/js/app/nginx/access/list/item.ejs index 613f62087..fe043c98e 100644 --- a/src/frontend/js/app/nginx/access/list/item.ejs +++ b/frontend/js/app/nginx/access/list/item.ejs @@ -1,6 +1,6 @@ -
- +
+
@@ -14,6 +14,16 @@ <%- 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}) %> @@ -22,6 +32,7 @@