mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-14 11:28:49 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70163a66fb | ||
|
|
6e82161987 | ||
|
|
f650137c84 | ||
|
|
02d3093d88 | ||
|
|
ab026e5e18 | ||
|
|
5a2548c89d | ||
|
|
5b1f0cead1 |
No files matched your search
@@ -1,7 +0,0 @@
|
|||||||
# Force LF everywhere: these files run inside Linux containers
|
|
||||||
* text=auto eol=lf
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.ico binary
|
|
||||||
*.woff binary
|
|
||||||
*.woff2 binary
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<!--
|
|
||||||
|
|
||||||
WARNINGS:
|
|
||||||
|
|
||||||
1. An AI bot will comment on this PR! If you don't get a perfect score or don't
|
|
||||||
address any issues raised, your PR won't get merged!
|
|
||||||
|
|
||||||
2. Any PR without a description will be closed. The title is not enough!
|
|
||||||
|
|
||||||
3. Don't go creating a duplicate PR! Check that someone hasn't already created
|
|
||||||
something that tackles your fix and if so, help them first
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Why
|
|
||||||
|
|
||||||
<!-- Provide a brief description of WHY you are making your changes -->
|
|
||||||
|
|
||||||
<!-- Consider if you are changing the API, then go in to detail why and/or if
|
|
||||||
you change will break API for existing users -->
|
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
|
|
||||||
<!-- Mark the relevant options with an "x" -->
|
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
|
||||||
- [ ] New feature (non-breaking change that adds functionality)
|
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
|
||||||
- [ ] Documentation update
|
|
||||||
- [ ] Code refactoring
|
|
||||||
- [ ] API changes
|
|
||||||
- [ ] Performance improvement
|
|
||||||
- [ ] Test addition or update
|
|
||||||
|
|
||||||
## AI Usage
|
|
||||||
|
|
||||||
<!-- Mark the relevant options with an "x" -->
|
|
||||||
|
|
||||||
- [ ] AI was used to write this
|
|
||||||
- [ ] AI was used to review this
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/backend"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
dev-patch-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/frontend"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
dev-patch-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/docs"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
dev-patch-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/test"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
dev-patch-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/docker"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
groups:
|
|
||||||
updates:
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name: 'Close stale issues and PRs'
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 1 * * *'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v11
|
|
||||||
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
|
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
.qodo
|
|
||||||
._*
|
._*
|
||||||
.vscode
|
.vscode
|
||||||
certbot-help.txt
|
certbot-help.txt
|
||||||
test/node_modules
|
|
||||||
*/node_modules
|
|
||||||
docker/dev/dnsrouter-config.json.tmp
|
|
||||||
docker/dev/resolv.conf
|
|
||||||
.claude
|
|
||||||
test/__pycache__
|
|
||||||
|
|
||||||
Vendored
+252
@@ -0,0 +1,252 @@
|
|||||||
|
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('/', '-')}"
|
||||||
|
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
|
||||||
|
COMPOSE_FILE = 'docker/docker-compose.ci.yml'
|
||||||
|
COMPOSE_INTERACTIVE_NO_CLI = 1
|
||||||
|
BUILDX_NAME = "${COMPOSE_PROJECT_NAME}"
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Environment') {
|
||||||
|
parallel {
|
||||||
|
stage('Master') {
|
||||||
|
when {
|
||||||
|
branch 'master'
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Other') {
|
||||||
|
when {
|
||||||
|
not {
|
||||||
|
branch 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
// Defaults to the Branch name, which is applies to all branches AND pr's
|
||||||
|
env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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('Frontend') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/frontend-build'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Backend') {
|
||||||
|
steps {
|
||||||
|
echo 'Checking Syntax ...'
|
||||||
|
// See: https://github.com/yarnpkg/yarn/issues/3254
|
||||||
|
sh '''docker run --rm \\
|
||||||
|
-v "$(pwd)/backend:/app" \\
|
||||||
|
-v "$(pwd)/global:/app/global" \\
|
||||||
|
-w /app \\
|
||||||
|
node:latest \\
|
||||||
|
sh -c "yarn install && yarn eslint . && rm -rf node_modules"
|
||||||
|
'''
|
||||||
|
|
||||||
|
echo 'Docker Build ...'
|
||||||
|
sh '''docker build --pull --no-cache --squash --compress \\
|
||||||
|
-t "${IMAGE}:ci-${BUILD_NUMBER}" \\
|
||||||
|
-f docker/Dockerfile \\
|
||||||
|
--build-arg TARGETPLATFORM=linux/amd64 \\
|
||||||
|
--build-arg BUILDPLATFORM=linux/amd64 \\
|
||||||
|
--build-arg BUILD_VERSION="${BUILD_VERSION}" \\
|
||||||
|
--build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\
|
||||||
|
--build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\
|
||||||
|
.
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Integration Tests Sqlite') {
|
||||||
|
steps {
|
||||||
|
// Bring up a stack
|
||||||
|
sh 'docker-compose up -d fullstack-sqlite'
|
||||||
|
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120'
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
sh 'rm -rf test/results'
|
||||||
|
sh 'docker-compose up cypress-sqlite'
|
||||||
|
// Get results
|
||||||
|
sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Dumps to analyze later
|
||||||
|
sh 'mkdir -p debug'
|
||||||
|
sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz'
|
||||||
|
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
|
||||||
|
// Cypress videos and screenshot artifacts
|
||||||
|
dir(path: 'test/results') {
|
||||||
|
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
|
||||||
|
}
|
||||||
|
junit 'test/results/junit/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Integration Tests Mysql') {
|
||||||
|
steps {
|
||||||
|
// Bring up a stack
|
||||||
|
sh 'docker-compose up -d fullstack-mysql'
|
||||||
|
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120'
|
||||||
|
|
||||||
|
// Run tests
|
||||||
|
sh 'rm -rf test/results'
|
||||||
|
sh 'docker-compose up cypress-mysql'
|
||||||
|
// Get results
|
||||||
|
sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/'
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// Dumps to analyze later
|
||||||
|
sh 'mkdir -p debug'
|
||||||
|
sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz'
|
||||||
|
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
|
||||||
|
// Cypress videos and screenshot artifacts
|
||||||
|
dir(path: 'test/results') {
|
||||||
|
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
|
||||||
|
}
|
||||||
|
junit 'test/results/junit/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Docs') {
|
||||||
|
when {
|
||||||
|
not {
|
||||||
|
equals expected: 'UNSTABLE', actual: currentBuild.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
dir(path: 'docs') {
|
||||||
|
sh 'yarn install'
|
||||||
|
sh 'yarn build'
|
||||||
|
}
|
||||||
|
|
||||||
|
dir(path: 'docs/.vuepress/dist') {
|
||||||
|
sh 'tar -czf ../../docs.tgz *'
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('MultiArch Build') {
|
||||||
|
when {
|
||||||
|
not {
|
||||||
|
equals expected: 'UNSTABLE', actual: currentBuild.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
|
||||||
|
// Docker Login
|
||||||
|
sh "docker login -u '${duser}' -p '${dpass}'"
|
||||||
|
// Buildx with push from cache
|
||||||
|
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('Docs Deploy') {
|
||||||
|
when {
|
||||||
|
allOf {
|
||||||
|
branch 'master'
|
||||||
|
not {
|
||||||
|
equals expected: 'UNSTABLE', actual: currentBuild.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) {
|
||||||
|
sh """docker run --rm \\
|
||||||
|
--name \${COMPOSE_PROJECT_NAME}-docs-upload \\
|
||||||
|
-e S3_BUCKET=jc21-npm-site \\
|
||||||
|
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
|
||||||
|
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
|
||||||
|
-v \$(pwd):/app \\
|
||||||
|
-w /app \\
|
||||||
|
jc21/ci-tools \\
|
||||||
|
scripts/docs-upload /app/docs/.vuepress/dist/
|
||||||
|
"""
|
||||||
|
|
||||||
|
sh """docker run --rm \\
|
||||||
|
--name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\
|
||||||
|
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
|
||||||
|
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
|
||||||
|
jc21/ci-tools \\
|
||||||
|
aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*'
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage('PR Comment') {
|
||||||
|
when {
|
||||||
|
allOf {
|
||||||
|
changeRequest()
|
||||||
|
not {
|
||||||
|
equals expected: 'UNSTABLE', actual: currentBuild.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30'
|
||||||
|
sh 'echo Reverting ownership'
|
||||||
|
sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data'
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
juxtapose event: 'success'
|
||||||
|
sh 'figlet "SUCCESS"'
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
|
||||||
|
juxtapose event: 'failure'
|
||||||
|
sh 'figlet "FAILURE"'
|
||||||
|
}
|
||||||
|
unstable {
|
||||||
|
archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
|
||||||
|
juxtapose event: 'unstable'
|
||||||
|
sh 'figlet "UNSTABLE"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getVersion() {
|
||||||
|
ver = sh(script: 'cat .version', returnStdout: true)
|
||||||
|
return ver.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
def getCommit() {
|
||||||
|
ver = sh(script: 'git log -n 1 --format=%h', returnStdout: true)
|
||||||
|
return ver.trim()
|
||||||
|
}
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://nginxproxymanager.com/github.png">
|
<img src="https://nginxproxymanager.com/github.png">
|
||||||
<br><br>
|
<br><br>
|
||||||
<img src="https://img.shields.io/badge/version-2.17.0-green.svg?style=for-the-badge">
|
<img src="https://img.shields.io/badge/version-2.9.9-green.svg?style=for-the-badge">
|
||||||
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
|
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
|
||||||
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
|
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
|
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
|
||||||
<img src="https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge">
|
<img src="https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge">
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://ci.nginxproxymanager.com/blue/organizations/jenkins/nginx-proxy-manager/branches/">
|
||||||
|
<img src="https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci.nginxproxymanager.com%2Fjob%2Fnginx-proxy-manager%2Fjob%2Fmaster&style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
<a href="https://gitter.im/nginx-proxy-manager/community">
|
||||||
|
<img alt="Gitter" src="https://img.shields.io/gitter/room/nginx-proxy-manager/community?style=for-the-badge">
|
||||||
|
</a>
|
||||||
|
<a href="https://reddit.com/r/nginxproxymanager">
|
||||||
|
<img alt="Reddit" src="https://img.shields.io/reddit/subreddit-subscribers/nginxproxymanager?label=Reddit%20Community&style=for-the-badge">
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
This project comes as a pre-built Docker image that enables you to easily forward to your websites
|
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.
|
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
|
||||||
|
|
||||||
- [Quick Setup](#quick-setup)
|
- [Quick Setup](#quick-setup)
|
||||||
@@ -19,10 +28,10 @@ running at home or otherwise, including free SSL, without having to know too muc
|
|||||||
|
|
||||||
## Project Goal
|
## Project Goal
|
||||||
|
|
||||||
I created this project to fill a personal need to provide users with an easy way to accomplish reverse
|
I created this project to fill a personal need to provide users with a 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.
|
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
|
While there might be advanced options they are optional and the project should be as simple as possible
|
||||||
so that the barrier to entry here is low.
|
so that the barrier for entry here is low.
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/jc21" target="_blank"><img src="http://public.jc21.com/github/by-me-a-coffee.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
<a href="https://www.buymeacoffee.com/jc21" target="_blank"><img src="http://public.jc21.com/github/by-me-a-coffee.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
||||||
|
|
||||||
@@ -30,54 +39,66 @@ so that the barrier to entry here is low.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Beautiful and Secure Admin Interface based on [Tabler](https://tabler.github.io/)
|
- 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
|
- 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
|
- Free SSL using Let's Encrypt or provide your own custom SSL certificates
|
||||||
- Access Lists and basic HTTP Authentication for your hosts
|
- Access Lists and basic HTTP Authentication for your hosts
|
||||||
- Advanced Nginx configuration available for super users
|
- Advanced Nginx configuration available for super users
|
||||||
- User management, permissions, and audit log
|
- User management, permissions and audit log
|
||||||
|
|
||||||
::: warning
|
|
||||||
`armv7` is no longer supported in version 2.14+. This is due to Nodejs dropping support for armhf. Please
|
|
||||||
use the `2.13.7` image tag if this applies to you.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Hosting your home network
|
## Hosting your home network
|
||||||
|
|
||||||
I won't go into too much detail here, but here are the basics for someone new to this self-hosted world.
|
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
|
1. Your home router will have a Port Forwarding section somewhere. Log in and find it
|
||||||
2. Add port forwarding for ports 80 and 443 to the server hosting this project
|
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
|
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)
|
||||||
- DuckDNS
|
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
|
||||||
- [Amazon Route53](https://github.com/jc21/route53-ddns)
|
|
||||||
- [Cloudflare](https://github.com/jc21/cloudflare-ddns)
|
|
||||||
4. Use the Nginx Proxy Manager as your gateway to forward to your other web-based services
|
|
||||||
|
|
||||||
## Quick Setup
|
## Quick Setup
|
||||||
|
|
||||||
1. [Install Docker](https://docs.docker.com/install/)
|
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:
|
2. Create a docker-compose.yml file similar to this:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: 'docker.io/jc21/nginx-proxy-manager:latest'
|
image: 'jc21/nginx-proxy-manager:latest'
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '80:80'
|
- '80:80'
|
||||||
- '81:81'
|
- '81:81'
|
||||||
- '443:443'
|
- '443:443'
|
||||||
|
environment:
|
||||||
|
DB_MYSQL_HOST: "db"
|
||||||
|
DB_MYSQL_PORT: 3306
|
||||||
|
DB_MYSQL_USER: "npm"
|
||||||
|
DB_MYSQL_PASSWORD: "npm"
|
||||||
|
DB_MYSQL_NAME: "npm"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./letsencrypt:/etc/letsencrypt
|
- ./letsencrypt:/etc/letsencrypt
|
||||||
|
db:
|
||||||
|
image: 'jc21/mariadb-aria:latest'
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: 'npm'
|
||||||
|
MYSQL_DATABASE: 'npm'
|
||||||
|
MYSQL_USER: 'npm'
|
||||||
|
MYSQL_PASSWORD: 'npm'
|
||||||
|
volumes:
|
||||||
|
- ./data/mysql:/var/lib/mysql
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.
|
3. Bring up your stack
|
||||||
|
|
||||||
3. Bring up your stack by running
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Log in to the Admin UI
|
4. Log in to the Admin UI
|
||||||
@@ -87,25 +108,406 @@ 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)
|
[http://127.0.0.1:81](http://127.0.0.1:81)
|
||||||
|
|
||||||
|
Default Admin User:
|
||||||
|
```
|
||||||
|
Email: admin@example.com
|
||||||
|
Password: changeme
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
Immediately after logging in with this default user you will be asked to modify your details and change your password.
|
||||||
|
|
||||||
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
|
## Contributors
|
||||||
|
|
||||||
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).
|
Special thanks to the following contributors:
|
||||||
|
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
## Getting Support
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)
|
<tr>
|
||||||
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)
|
<td align="center">
|
||||||
3. [Reddit](https://reddit.com/r/nginxproxymanager)
|
<a href="https://github.com/Subv">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Sebastian Valle</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Indemnity83">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Kyle Klaus</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/theraw">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/32969774?s=460&u=6b359971e15685fb0359e6a8c065a399b40dc228&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>ƬHE ЯAW</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/spalger">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/1329312?s=400&u=565223e38f1c052afb4c5dcca3fcf1c63ba17ae7&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Spencer</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Xantios">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/1507836?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Xantios Krugor</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/dpanesso">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/2687121?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>David Panesso</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/IronTooch">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/27360514?s=460&u=69bf854a6647c55725f62ecb8d39249c6c0b2602&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>IronTooch</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/damianog">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/2786682?s=460&u=76c6136fae797abb76b951cd8a246dcaecaf21af&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Damiano</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/tfmm">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/6880538?s=460&u=ce0160821cc4aa802df8395200f2d4956a5bc541&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Russ</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/margaale">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/20794934?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Marcelo Castagna</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Steven-Harris">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/7720242?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Steven Harris</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/jlesage">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/1791123?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Jocelyn Le Sage</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/cmer">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/412?s=460&u=67dd8b2e3661bfd6f68ec1eaa5b9821bd8a321cd&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Carl Mercier</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/the1ts">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/84956?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Paul Mansfield</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/OhHeyAlan">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/11955126?s=460&u=fbaa5a1a4f73ef8960132c703349bfd037fe2630&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>OhHeyAlan</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/dogmatic69">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Carl Sutton</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/tg44">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/31839?s=460&u=ad32f4cadfef5e5fb09cdfa4b7b7b36a99ba6811&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Gergő Törcsvári</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/vrenjith">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/2093241?s=460&u=96ce93a9bebabdd0a60a2dc96cd093a41d5edaba&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>vrenjith</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/duhruh">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>David Rivera</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/jipjan">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Jaap-Jan de Wit</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/jmwebslave">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/6118262?s=460&u=7db409c47135b1e141c366bbb03ed9fae6ac2638&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>James Morgan</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/chaptergy">
|
||||||
|
<img src="https://avatars2.githubusercontent.com/u/26956711?s=460&u=7d9adebabb6b4e7af7cb05d98d751087a372304b&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>chaptergy</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Philip-Mooney">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/48624631?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Philip Mooney</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/WaterCalm">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/23502129?s=400&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>WaterCalm</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/lebrou34">
|
||||||
|
<img src="https://avatars1.githubusercontent.com/u/16373103?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>lebrou34</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/lightglitch">
|
||||||
|
<img src="https://avatars0.githubusercontent.com/u/196953?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Mário Franco</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/klutchell">
|
||||||
|
<img src="https://avatars3.githubusercontent.com/u/20458272?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Kyle Harding</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ahgraber">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/24922003?s=460&u=8376c9f00af9b6057ba4d2fb03b4f1b20a75277f&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Alex Graber</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/MooBaloo">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/9493496?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>MooBaloo</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Shuro">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/944030?s=460&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Shuro</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/lorisbergeron">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/51918567?s=460&u=778e4ff284b7d7304450f98421c99f79298371fb&v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Loris Bergeron</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/hepelayo">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/8243119?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>hepelayo</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/jonasled">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/46790650?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Jonas Leder</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/stegmannb">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/12850482?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Bastian Stegmann</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Stealthii">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/998920?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Stealthii</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/thegamingninja">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/8020534?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>THEGamingninja</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/italobb">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/1801687?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Italo Borssatto</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/GurjinderSingh">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/3470709?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Gurjinder Singh</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/phantomski77">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/69464125?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>David Dosoudil</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ijaron">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/5156472?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>ijaron</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/nielscil">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/9073152?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Niels Bouma</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ogarai">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/2949572?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Orko Garai</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/baruffaldi">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/36949?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Filippo Baruffaldi</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/bikram990">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/6782131?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Bikramjeet Singh</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/razvanstoica89">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/28236583?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Razvan Stoica</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/psharma04">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/22587474?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>RBXII3</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/demize">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/264914?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>demize</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/PUP-Loki">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/75944209?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>PUP-Loki</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/DSorlov">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/8133650?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Daniel Sörlöv</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Theyooo">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/58510131?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Theyooo</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/mrdink">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/514751?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Justin Peacock</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ChrisTracy">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/58871574?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Chris Tracy</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/Fuechslein">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/15112818?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Fuechslein</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/nightah">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/3339418?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Amir Zarrinkafsh</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/gabbe">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/156397?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>gabbe</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/bmbvenom">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/20530371?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>bmbvenom</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/FMeinicke">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/42121639?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Florian Meinicke</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/ssrahul96">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/15570570?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Rahul Somasundaram</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/BjoernAkAManf">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/833043?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Björn Heinrichs</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/realJoshByrnes">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/204185?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>Josh Byrnes</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/bergi9">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/5556750?v=4" width="80" alt=""/>
|
||||||
|
<br /><sub><b>bergi9</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!-- markdownlint-enable -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Only the latest stable release receives security updates.
|
|
||||||
Older versions are not actively maintained.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | --------- |
|
|
||||||
| 2.16.x (latest) | :white_check_mark: |
|
|
||||||
| < 2.16.0 | :x: |
|
|
||||||
|
|
||||||
Docker images: `jc21/nginx-proxy-manager:latest`, `jc21/nginx-proxy-manager:2`
|
|
||||||
|
|
||||||
See all releases: https://github.com/NginxProxyManager/nginx-proxy-manager/releases
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
**Do NOT open a public GitHub Issue to report a security vulnerability.**
|
|
||||||
|
|
||||||
Use GitHub's private vulnerability reporting:
|
|
||||||
https://github.com/NginxProxyManager/nginx-proxy-manager/security/advisories/new
|
|
||||||
|
|
||||||
Please include:
|
|
||||||
- Affected version (Docker image tag or release)
|
|
||||||
- Description of the vulnerability
|
|
||||||
- Steps to reproduce
|
|
||||||
- Potential impact
|
|
||||||
|
|
||||||
Once a fix is available, a public GitHub Security Advisory will be published.
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 320,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"proseWrap": "always"
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-46
@@ -1,12 +1,8 @@
|
|||||||
import bodyParser from "body-parser";
|
const express = require('express');
|
||||||
import compression from "compression";
|
const bodyParser = require('body-parser');
|
||||||
import express from "express";
|
const fileUpload = require('express-fileupload');
|
||||||
import fileUpload from "express-fileupload";
|
const compression = require('compression');
|
||||||
import { isDebugMode } from "./lib/config.js";
|
const log = require('./logger').express;
|
||||||
import cors from "./lib/express/cors.js";
|
|
||||||
import jwt from "./lib/express/jwt.js";
|
|
||||||
import { debug, express as logger } from "./logger.js";
|
|
||||||
import mainRoutes from "./routes/main.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App
|
* App
|
||||||
@@ -14,7 +10,7 @@ import mainRoutes from "./routes/main.js";
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.use(fileUpload());
|
app.use(fileUpload());
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({extended: true}));
|
||||||
|
|
||||||
// Gzip
|
// Gzip
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
@@ -23,70 +19,70 @@ app.use(compression());
|
|||||||
* General Logging, BEFORE routes
|
* General Logging, BEFORE routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.disable("x-powered-by");
|
app.disable('x-powered-by');
|
||||||
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
|
app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
||||||
app.enable("strict routing");
|
app.enable('strict routing');
|
||||||
|
|
||||||
// pretty print JSON when not live
|
// pretty print JSON when not live
|
||||||
if (isDebugMode()) {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.set("json spaces", 2);
|
app.set('json spaces', 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS for everything
|
// CORS for everything
|
||||||
app.use(cors);
|
app.use(require('./lib/express/cors'));
|
||||||
|
|
||||||
// General security/cache related headers + server header
|
// General security/cache related headers + server header
|
||||||
app.use((_, res, next) => {
|
app.use(function (req, res, next) {
|
||||||
let x_frame_options = "DENY";
|
let x_frame_options = 'DENY';
|
||||||
|
|
||||||
if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) {
|
if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) {
|
||||||
x_frame_options = process.env.X_FRAME_OPTIONS;
|
x_frame_options = process.env.X_FRAME_OPTIONS;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.set({
|
res.set({
|
||||||
"X-XSS-Protection": "1; mode=block",
|
'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
|
||||||
"X-Content-Type-Options": "nosniff",
|
'X-XSS-Protection': '1; mode=block',
|
||||||
"X-Frame-Options": x_frame_options,
|
'X-Content-Type-Options': 'nosniff',
|
||||||
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
|
'X-Frame-Options': x_frame_options,
|
||||||
Pragma: "no-cache",
|
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
|
||||||
Expires: 0,
|
Pragma: 'no-cache',
|
||||||
|
Expires: 0
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(jwt());
|
app.use(require('./lib/express/jwt')());
|
||||||
app.use("/", mainRoutes);
|
app.use('/', require('./routes/api/main'));
|
||||||
|
|
||||||
// production error handler
|
// production error handler
|
||||||
// no stacktraces leaked to user
|
// no stacktraces leaked to user
|
||||||
app.use((err, req, res, _) => {
|
// eslint-disable-next-line
|
||||||
const payload = {
|
app.use(function (err, req, res, next) {
|
||||||
|
|
||||||
|
let payload = {
|
||||||
error: {
|
error: {
|
||||||
code: err.status || 500,
|
code: err.status,
|
||||||
message: err.public ? err.message : "Internal Error",
|
message: err.public ? err.message : 'Internal Error'
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof err.message_i18n !== "undefined") {
|
if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
|
||||||
payload.error.message_i18n = err.message_i18n;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
|
|
||||||
payload.debug = {
|
payload.debug = {
|
||||||
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
|
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
|
||||||
previous: err.previous,
|
previous: err.previous
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not every error is worth logging - but this is good for now until it gets annoying.
|
// Not every error is worth logging - but this is good for now until it gets annoying.
|
||||||
if (typeof err.stack !== "undefined" && err.stack) {
|
if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
|
||||||
debug(logger, err.stack);
|
log.debug(err);
|
||||||
if (typeof err.public === "undefined" || !err.public) {
|
} else if (typeof err.stack !== 'undefined' && err.stack && (typeof err.public == 'undefined' || !err.public)) {
|
||||||
logger.warn(err.message);
|
log.warn(err.message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(err.status || 500).send(payload);
|
res
|
||||||
|
.status(err.status || 500)
|
||||||
|
.send(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
module.exports = app;
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.6.6/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false,
|
|
||||||
"includes": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "!**/dist/**/*"]
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "tab",
|
|
||||||
"indentWidth": 4,
|
|
||||||
"lineWidth": 120,
|
|
||||||
"formatWithErrors": true
|
|
||||||
},
|
|
||||||
"assist": {
|
|
||||||
"actions": {
|
|
||||||
"source": {
|
|
||||||
"organizeImports": {
|
|
||||||
"level": "on",
|
|
||||||
"options": {
|
|
||||||
"groups": [
|
|
||||||
":BUN:",
|
|
||||||
":NODE:",
|
|
||||||
["npm:*", "npm:*/**"],
|
|
||||||
":PACKAGE_WITH_PROTOCOL:",
|
|
||||||
":URL:",
|
|
||||||
":PACKAGE:",
|
|
||||||
["/src/*", "/src/**"],
|
|
||||||
["/**"],
|
|
||||||
["#*", "#*/**"],
|
|
||||||
":PATH:"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"preset": "recommended",
|
|
||||||
"correctness": {
|
|
||||||
"useUniqueElementIds": "off"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "off"
|
|
||||||
},
|
|
||||||
"performance": {
|
|
||||||
"noDelete": "off"
|
|
||||||
},
|
|
||||||
"nursery": "off",
|
|
||||||
"a11y": {
|
|
||||||
"useSemanticElements": "off",
|
|
||||||
"useValidAnchor": "off"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"noParameterAssign": "error",
|
|
||||||
"useAsConstAssertion": "error",
|
|
||||||
"useDefaultParameterLast": "error",
|
|
||||||
"useEnumInitializers": "error",
|
|
||||||
"useSelfClosingElements": "error",
|
|
||||||
"useSingleVarDeclarator": "error",
|
|
||||||
"noUnusedTemplateLiteral": "error",
|
|
||||||
"useNumberNamespace": "error",
|
|
||||||
"noInferrableTypes": "error",
|
|
||||||
"noUselessElse": "error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Certbot dns-plugins
|
|
||||||
|
|
||||||
This file contains info about available Certbot DNS plugins.
|
|
||||||
This only works for plugins which use the standard argument structure, so:
|
|
||||||
--authenticator <plugin-name> --<plugin-name>-credentials <FILE> --<plugin-name>-propagation-seconds <number>
|
|
||||||
|
|
||||||
File Structure:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cloudflare": {
|
|
||||||
"display_name": "Name displayed to the user",
|
|
||||||
"package_name": "Package name in PyPi repo",
|
|
||||||
"version_requirement": "Optional package version requirements (e.g. ==1.3 or >=1.2,<2.0, see https://www.python.org/dev/peps/pep-0440/#version-specifiers)",
|
|
||||||
"dependencies": "Additional dependencies, space separated (as you would pass it to pip install)",
|
|
||||||
"credentials": "Template of the credentials file",
|
|
||||||
"full_plugin_name": "The full plugin name as used in the commandline with certbot, e.g. 'dns-njalla'"
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
{
|
|
||||||
"acmedns": {
|
|
||||||
"credentials": "dns_acmedns_api_url = http://acmedns-server/\ndns_acmedns_registration_file = /data/acme-registration.json",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-acmedns",
|
|
||||||
"name": "ACME-DNS",
|
|
||||||
"package_name": "certbot-dns-acmedns",
|
|
||||||
"version": "~=0.1.0"
|
|
||||||
},
|
|
||||||
"active24": {
|
|
||||||
"credentials": "dns_active24_api_key = <identifier>\ndns_active24_secret = <secret>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-active24",
|
|
||||||
"name": "Active24",
|
|
||||||
"package_name": "certbot-dns-active24",
|
|
||||||
"version": "~=2.0.0"
|
|
||||||
},
|
|
||||||
"aliyun": {
|
|
||||||
"credentials": "dns_aliyun_access_key = 12345678\ndns_aliyun_access_key_secret = 1234567890abcdef1234567890abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-aliyun",
|
|
||||||
"name": "Aliyun",
|
|
||||||
"package_name": "certbot-dns-aliyun",
|
|
||||||
"version": "~=2.0.0"
|
|
||||||
},
|
|
||||||
"arvan": {
|
|
||||||
"credentials": "dns_arvan_key = Apikey xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-arvan",
|
|
||||||
"name": "ArvanCloud",
|
|
||||||
"package_name": "certbot-dns-arvan",
|
|
||||||
"version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"azure": {
|
|
||||||
"credentials": "# Docs: https://cloudchristoph.github.io/certbot-dns-azure-modern/\n# Service principal with the \"DNS Zone Contributor\" role on the DNS zone\ndns_azure_sp_client_id = 912ce44a-0156-4669-ae22-c16a17d34ca5\ndns_azure_sp_client_secret = example-client-secret-not-real\ndns_azure_tenant_id = ed1090f3-ab18-4b12-816c-599af8a88cf7\n# One line per DNS zone, format ZONE_NAME:RESOURCE_GROUP_ID\ndns_azure_zone1 = example.com:/subscriptions/c135abce-d87d-48df-936c-15596c6968a5/resourceGroups/rg-dns",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-azure",
|
|
||||||
"name": "Azure",
|
|
||||||
"package_name": "certbot-dns-azure-modern",
|
|
||||||
"version": "~=2.8.0"
|
|
||||||
},
|
|
||||||
"baidu": {
|
|
||||||
"credentials": "dns_baidu_access_key = 12345678\ndns_baidu_secret_key = 1234567890abcdef1234567890abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-baidu",
|
|
||||||
"name": "baidu",
|
|
||||||
"package_name": "certbot-dns-baidu-bcd",
|
|
||||||
"version": "~=2.0.0"
|
|
||||||
},
|
|
||||||
"beget": {
|
|
||||||
"credentials": "# Beget API credentials used by Certbot\n# Find on Settings -> Security -> BegetAPI\ndns_beget_plugin_username = username\ndns_beget_plugin_password = password",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-beget-plugin",
|
|
||||||
"name": "Beget",
|
|
||||||
"package_name": "certbot-dns-beget-plugin",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"bunny": {
|
|
||||||
"credentials": "# Bunny API token used by Certbot (see https://dash.bunny.net/account/settings)\ndns_bunny_api_key = xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-bunny",
|
|
||||||
"name": "bunny.net",
|
|
||||||
"package_name": "certbot-dns-bunny",
|
|
||||||
"version": "~=0.0.9"
|
|
||||||
},
|
|
||||||
"cdmon": {
|
|
||||||
"credentials": "dns_cdmon_api_key=your-cdmon-api-token\ndns_cdmon_domain=your_domain_is_optional",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-cdmon",
|
|
||||||
"name": "cdmon",
|
|
||||||
"package_name": "certbot-dns-cdmon",
|
|
||||||
"version": "~=0.4.1"
|
|
||||||
},
|
|
||||||
"cloudflare": {
|
|
||||||
"credentials": "# Cloudflare API token\ndns_cloudflare_api_token=0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-cloudflare",
|
|
||||||
"name": "Cloudflare",
|
|
||||||
"package_name": "certbot-dns-cloudflare",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"cloudns": {
|
|
||||||
"credentials": "# Target user ID (see https://www.cloudns.net/api-settings/)\n\tdns_cloudns_auth_id=1234\n\t# Alternatively, one of the following two options can be set:\n\t# dns_cloudns_sub_auth_id=1234\n\t# dns_cloudns_sub_auth_user=foobar\n\n\t# API password\n\tdns_cloudns_auth_password=password1",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-cloudns",
|
|
||||||
"name": "ClouDNS",
|
|
||||||
"package_name": "certbot-dns-cloudns",
|
|
||||||
"version": "~=0.7.0"
|
|
||||||
},
|
|
||||||
"cloudxns": {
|
|
||||||
"credentials": "dns_cloudxns_api_key = 1234567890abcdef1234567890abcdef\ndns_cloudxns_secret_key = 1122334455667788",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-cloudxns",
|
|
||||||
"name": "CloudXNS",
|
|
||||||
"package_name": "certbot-dns-cloudxns",
|
|
||||||
"version": "~=1.32.0"
|
|
||||||
},
|
|
||||||
"constellix": {
|
|
||||||
"credentials": "dns_constellix_apikey = 5fb4e76f-ac91-43e5-f982458bc595\ndns_constellix_secretkey = 47d99fd0-32e7-4e07-85b46d08e70b\ndns_constellix_endpoint = https://api.dns.constellix.com/v1",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-constellix",
|
|
||||||
"name": "Constellix",
|
|
||||||
"package_name": "certbot-dns-constellix",
|
|
||||||
"version": "~=0.2.1"
|
|
||||||
},
|
|
||||||
"corenetworks": {
|
|
||||||
"credentials": "dns_corenetworks_username = asaHB12r\ndns_corenetworks_password = secure_password",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-corenetworks",
|
|
||||||
"name": "Core Networks",
|
|
||||||
"package_name": "certbot-dns-corenetworks",
|
|
||||||
"version": "~=0.1.4"
|
|
||||||
},
|
|
||||||
"cpanel": {
|
|
||||||
"credentials": "cpanel_url = https://cpanel.example.com:2083\ncpanel_username = your_username\ncpanel_password = your_password\ncpanel_token = your_api_token",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "cpanel",
|
|
||||||
"name": "cPanel",
|
|
||||||
"package_name": "certbot-dns-cpanel",
|
|
||||||
"version": "~=0.4.0"
|
|
||||||
},
|
|
||||||
"ddnss": {
|
|
||||||
"credentials": "dns_ddnss_token = YOUR_DDNSS_API_TOKEN",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-ddnss",
|
|
||||||
"name": "DDNSS",
|
|
||||||
"package_name": "certbot-dns-ddnss",
|
|
||||||
"version": "~=1.1.0"
|
|
||||||
},
|
|
||||||
"desec": {
|
|
||||||
"credentials": "dns_desec_token = YOUR_DESEC_API_TOKEN\ndns_desec_endpoint = https://desec.io/api/v1/",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-desec",
|
|
||||||
"name": "deSEC",
|
|
||||||
"package_name": "certbot-dns-desec",
|
|
||||||
"version": "~=1.3.2"
|
|
||||||
},
|
|
||||||
"digitalocean": {
|
|
||||||
"credentials": "dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-digitalocean",
|
|
||||||
"name": "DigitalOcean",
|
|
||||||
"package_name": "certbot-dns-digitalocean",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"directadmin": {
|
|
||||||
"credentials": "directadmin_url = https://my.directadminserver.com:2222\ndirectadmin_username = username\ndirectadmin_password = aSuperStrongPassword",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "directadmin",
|
|
||||||
"name": "DirectAdmin",
|
|
||||||
"package_name": "certbot-dns-directadmin",
|
|
||||||
"version": "~=0.0.23"
|
|
||||||
},
|
|
||||||
"dnsimple": {
|
|
||||||
"credentials": "dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-dnsimple",
|
|
||||||
"name": "DNSimple",
|
|
||||||
"package_name": "certbot-dns-dnsimple",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"dnsmadeeasy": {
|
|
||||||
"credentials": "dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\ndns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-dnsmadeeasy",
|
|
||||||
"name": "DNS Made Easy",
|
|
||||||
"package_name": "certbot-dns-dnsmadeeasy",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"dnsmulti": {
|
|
||||||
"credentials": "# See https://go-acme.github.io/lego/dns/#dns-providers for list of providers and their settings\n# Example provider configuration for DreamHost\n# dns_multi_provider = dreamhost\n# DREAMHOST_API_KEY = ABCDEFG1234",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-multi",
|
|
||||||
"name": "DnsMulti",
|
|
||||||
"package_name": "certbot-dns-multi",
|
|
||||||
"version": "~=5.6.0"
|
|
||||||
},
|
|
||||||
"dnspod": {
|
|
||||||
"credentials": "dns_dnspod_email = \"email@example.com\"\ndns_dnspod_api_token = \"id,key\"",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-dnspod",
|
|
||||||
"name": "DNSPod",
|
|
||||||
"package_name": "certbot-dns-dnspod",
|
|
||||||
"version": "~=0.1.0"
|
|
||||||
},
|
|
||||||
"domainoffensive": {
|
|
||||||
"credentials": "dns_domainoffensive_api_token = YOUR_DO_DE_AUTH_TOKEN",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-domainoffensive",
|
|
||||||
"name": "DomainOffensive (do.de)",
|
|
||||||
"package_name": "certbot-dns-domainoffensive",
|
|
||||||
"version": "~=2.0.0"
|
|
||||||
},
|
|
||||||
"domeneshop": {
|
|
||||||
"credentials": "dns_domeneshop_client_token=YOUR_DOMENESHOP_CLIENT_TOKEN\ndns_domeneshop_client_secret=YOUR_DOMENESHOP_CLIENT_SECRET",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-domeneshop",
|
|
||||||
"name": "Domeneshop",
|
|
||||||
"package_name": "certbot-dns-domeneshop",
|
|
||||||
"version": "~=0.2.8"
|
|
||||||
},
|
|
||||||
"duckdns": {
|
|
||||||
"credentials": "dns_duckdns_token=your-duckdns-token",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-duckdns",
|
|
||||||
"name": "DuckDNS",
|
|
||||||
"package_name": "certbot-dns-duckdns",
|
|
||||||
"version": "~=1.0"
|
|
||||||
},
|
|
||||||
"dynu": {
|
|
||||||
"credentials": "dns_dynu_auth_token = YOUR_DYNU_AUTH_TOKEN",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-dynu",
|
|
||||||
"name": "Dynu",
|
|
||||||
"package_name": "certbot-dns-dynu",
|
|
||||||
"version": "~=0.0.1"
|
|
||||||
},
|
|
||||||
"easydns": {
|
|
||||||
"credentials": "dns_easydns_usertoken = YOUR_EASYDNS_USERTOKEN\ndns_easydns_userkey = YOUR_EASYDNS_USERKEY\ndns_easydns_endpoint = https://rest.easydns.net",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-easydns",
|
|
||||||
"name": "easyDNS",
|
|
||||||
"package_name": "certbot-dns-easydns",
|
|
||||||
"version": "~=0.1.2"
|
|
||||||
},
|
|
||||||
"edgedns": {
|
|
||||||
"credentials": "client_token = YOUR_CLIENT_TOKEN\naccess_token = YOUR_ACCESS_TOKEN\nclient_secret = YOUR_CLIENT_SECRET\nhost = YOUR_HOST.luna.akamaiapis.net\n# Optional: account_key = YOUR_ACCOUNT_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "edgedns",
|
|
||||||
"name": "Akamai Edge DNS",
|
|
||||||
"package_name": "certbot-plugin-edgedns",
|
|
||||||
"version": "==0.3.0"
|
|
||||||
},
|
|
||||||
"edgeone": {
|
|
||||||
"credentials": "dns_edgeone_secret_id = YOUR_TENCENTCLOUD_SECRET_ID\ndns_edgeone_secret_key = YOUR_TENCENTCLOUD_SECRET_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-edgeone",
|
|
||||||
"name": "Tencent Cloud EdgeOne",
|
|
||||||
"package_name": "certbot-dns-edgeone",
|
|
||||||
"version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"eurodns": {
|
|
||||||
"credentials": "dns_eurodns_applicationId = myuser\ndns_eurodns_apiKey = mysecretpassword\ndns_eurodns_endpoint = https://rest-api.eurodns.com/dns-zones/",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-eurodns",
|
|
||||||
"name": "EuroDNS",
|
|
||||||
"package_name": "certbot-dns-eurodns",
|
|
||||||
"version": "~=1.8.2"
|
|
||||||
},
|
|
||||||
"firstdomains": {
|
|
||||||
"credentials": "dns_firstdomains_username = myremoteuser\ndns_firstdomains_password = verysecureremoteuserpassword",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-firstdomains",
|
|
||||||
"name": "First Domains",
|
|
||||||
"package_name": "certbot-dns-firstdomains",
|
|
||||||
"version": ">=1.0"
|
|
||||||
},
|
|
||||||
"freedns": {
|
|
||||||
"credentials": "dns_freedns_username = myremoteuser\ndns_freedns_password = verysecureremoteuserpassword",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-freedns",
|
|
||||||
"name": "FreeDNS",
|
|
||||||
"package_name": "certbot-dns-freedns",
|
|
||||||
"version": "~=0.1.0"
|
|
||||||
},
|
|
||||||
"gandi": {
|
|
||||||
"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-gandi",
|
|
||||||
"name": "Gandi Live DNS",
|
|
||||||
"package_name": "certbot-dns-gandi",
|
|
||||||
"version": "~=1.6.1"
|
|
||||||
},
|
|
||||||
"gcore": {
|
|
||||||
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-gcore",
|
|
||||||
"name": "Gcore DNS",
|
|
||||||
"package_name": "certbot-dns-gcore",
|
|
||||||
"version": "~=0.1.8"
|
|
||||||
},
|
|
||||||
"glesys": {
|
|
||||||
"credentials": "dns_glesys_user = CL00000\ndns_glesys_password = apikeyvalue",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-glesys",
|
|
||||||
"name": "Glesys",
|
|
||||||
"package_name": "certbot-dns-glesys",
|
|
||||||
"version": "~=2.1.0"
|
|
||||||
},
|
|
||||||
"godaddy": {
|
|
||||||
"credentials": "dns_godaddy_secret = 0123456789abcdef0123456789abcdef01234567\ndns_godaddy_key = abcdef0123456789abcdef01234567abcdef0123",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-godaddy",
|
|
||||||
"name": "GoDaddy",
|
|
||||||
"package_name": "certbot-dns-godaddy",
|
|
||||||
"version": "==2.8.0"
|
|
||||||
},
|
|
||||||
"google": {
|
|
||||||
"credentials": "{\n\"type\": \"service_account\",\n...\n}",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-google",
|
|
||||||
"name": "Google",
|
|
||||||
"package_name": "certbot-dns-google",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"googledomains": {
|
|
||||||
"credentials": "dns_google_domains_access_token = 0123456789abcdef0123456789abcdef01234567\ndns_google_domains_zone = \"example.com\"",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-google-domains",
|
|
||||||
"name": "GoogleDomainsDNS",
|
|
||||||
"package_name": "certbot-dns-google-domains",
|
|
||||||
"version": "~=0.1.5"
|
|
||||||
},
|
|
||||||
"he": {
|
|
||||||
"credentials": "dns_he_user = Me\ndns_he_pass = my HE password",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-he",
|
|
||||||
"name": "Hurricane Electric",
|
|
||||||
"package_name": "certbot-dns-he",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"he-ddns": {
|
|
||||||
"credentials": "dns_he_ddns_password = verysecurepassword",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-he-ddns",
|
|
||||||
"name": "Hurricane Electric - DDNS",
|
|
||||||
"package_name": "certbot-dns-he-ddns",
|
|
||||||
"version": "~=0.1.0"
|
|
||||||
},
|
|
||||||
"hetzner": {
|
|
||||||
"credentials": "dns_hetzner_api_token = 0123456789abcdef0123456789abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hetzner",
|
|
||||||
"name": "Hetzner",
|
|
||||||
"package_name": "certbot-dns-hetzner",
|
|
||||||
"version": "~=4.0.0"
|
|
||||||
},
|
|
||||||
"hetzner-cloud": {
|
|
||||||
"credentials": "dns_hetzner_cloud_api_token = your_api_token_here",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hetzner-cloud",
|
|
||||||
"name": "Hetzner Cloud",
|
|
||||||
"package_name": "certbot-dns-hetzner-cloud",
|
|
||||||
"version": "~=1.0.4"
|
|
||||||
},
|
|
||||||
"hostinger": {
|
|
||||||
"credentials": "dns_hostinger_api_token = 0123456789abcdef0123456789abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hostinger",
|
|
||||||
"name": "Hostinger.com",
|
|
||||||
"package_name": "certbot-dns-hostinger",
|
|
||||||
"version": "~=0.1.3"
|
|
||||||
},
|
|
||||||
"hostingnl": {
|
|
||||||
"credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hostingnl",
|
|
||||||
"name": "Hosting.nl",
|
|
||||||
"package_name": "certbot-dns-hostingnl",
|
|
||||||
"version": "~=0.1.5"
|
|
||||||
},
|
|
||||||
"hover": {
|
|
||||||
"credentials": "dns_hover_hoverurl = https://www.hover.com\ndns_hover_username = hover-admin-username\ndns_hover_password = hover-admin-password\ndns_hover_totpsecret = 2fa-totp-secret",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hover",
|
|
||||||
"name": "Hover",
|
|
||||||
"package_name": "certbot-dns-hover",
|
|
||||||
"version": "~=1.2.1"
|
|
||||||
},
|
|
||||||
"hosterby": {
|
|
||||||
"credentials": "dns_hosterby_access_key = YOUR_ACCESS_KEY\ndns_hosterby_secret_key = YOUR_SECRET_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-hosterby",
|
|
||||||
"name": "hoster.by",
|
|
||||||
"package_name": "certbot-dns-hosterby",
|
|
||||||
"version": "~=0.1.0"
|
|
||||||
},
|
|
||||||
"infomaniak": {
|
|
||||||
"credentials": "dns_infomaniak_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-infomaniak",
|
|
||||||
"name": "Infomaniak",
|
|
||||||
"package_name": "certbot-dns-infomaniak",
|
|
||||||
"version": "~=0.3.3"
|
|
||||||
},
|
|
||||||
"inwx": {
|
|
||||||
"credentials": "dns_inwx_url = https://api.domrobot.com/xmlrpc/\ndns_inwx_username = your_username\ndns_inwx_password = your_password\ndns_inwx_shared_secret = your_shared_secret optional",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-inwx",
|
|
||||||
"name": "INWX",
|
|
||||||
"package_name": "certbot-dns-inwx",
|
|
||||||
"version": "~=2.1.2"
|
|
||||||
},
|
|
||||||
"ionos": {
|
|
||||||
"credentials": "dns_ionos_prefix = myapikeyprefix\ndns_ionos_secret = verysecureapikeysecret\ndns_ionos_endpoint = https://api.hosting.ionos.com",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-ionos",
|
|
||||||
"name": "IONOS",
|
|
||||||
"package_name": "certbot-dns-ionos",
|
|
||||||
"version": "==2022.11.24"
|
|
||||||
},
|
|
||||||
"ip-projects": {
|
|
||||||
"credentials": "dns_ipp_acme_api_key = <API key>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-ipp",
|
|
||||||
"name": "ip-projects.de",
|
|
||||||
"package_name": "certbot-dns-ipp",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"ispconfig": {
|
|
||||||
"credentials": "dns_ispconfig_username = myremoteuser\ndns_ispconfig_password = verysecureremoteuserpassword\ndns_ispconfig_endpoint = https://localhost:8080",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-ispconfig",
|
|
||||||
"name": "ISPConfig",
|
|
||||||
"package_name": "certbot-dns-ispconfig",
|
|
||||||
"version": "~=0.2.0"
|
|
||||||
},
|
|
||||||
"isset": {
|
|
||||||
"credentials": "dns_isset_endpoint=\"https://customer.isset.net/api\"\ndns_isset_token=\"<token>\"",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-isset",
|
|
||||||
"name": "Isset",
|
|
||||||
"package_name": "certbot-dns-isset",
|
|
||||||
"version": "~=0.0.3"
|
|
||||||
},
|
|
||||||
"joker": {
|
|
||||||
"credentials": "dns_joker_username = <Dynamic DNS Authentication Username>\ndns_joker_password = <Dynamic DNS Authentication Password>\ndns_joker_domain = <Dynamic DNS Domain>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-joker",
|
|
||||||
"name": "Joker",
|
|
||||||
"package_name": "certbot-dns-joker",
|
|
||||||
"version": "~=1.1.0"
|
|
||||||
},
|
|
||||||
"kas": {
|
|
||||||
"credentials": "dns_kas_user = your_kas_user\ndns_kas_password = your_kas_password",
|
|
||||||
"dependencies": "kasserver",
|
|
||||||
"full_plugin_name": "dns-kas",
|
|
||||||
"name": "All-Inkl",
|
|
||||||
"package_name": "certbot-dns-kas",
|
|
||||||
"version": "~=0.1.1"
|
|
||||||
},
|
|
||||||
"leaseweb": {
|
|
||||||
"credentials": "dns_leaseweb_api_token = 01234556789",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-leaseweb",
|
|
||||||
"name": "LeaseWeb",
|
|
||||||
"package_name": "certbot-dns-leaseweb",
|
|
||||||
"version": "~=1.0.3"
|
|
||||||
},
|
|
||||||
"linode": {
|
|
||||||
"credentials": "dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\ndns_linode_version = [<blank>|3|4]",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-linode",
|
|
||||||
"name": "Linode",
|
|
||||||
"package_name": "certbot-dns-linode",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"loopia": {
|
|
||||||
"credentials": "dns_loopia_user = user@loopiaapi\ndns_loopia_password = abcdef0123456789abcdef01234567abcdef0123",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-loopia",
|
|
||||||
"name": "Loopia",
|
|
||||||
"package_name": "certbot-dns-loopia",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"luadns": {
|
|
||||||
"credentials": "dns_luadns_email = user@example.com\ndns_luadns_token = 0123456789abcdef0123456789abcdef",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-luadns",
|
|
||||||
"name": "LuaDNS",
|
|
||||||
"package_name": "certbot-dns-luadns",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"mchost24": {
|
|
||||||
"credentials": "# Obtain API token using https://github.com/JoeJoeTV/mchost24-api-python\ndns_mchost24_api_token=<insert obtained API token here>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-mchost24",
|
|
||||||
"name": "MC-HOST24",
|
|
||||||
"package_name": "certbot-dns-mchost24",
|
|
||||||
"version": ""
|
|
||||||
},
|
|
||||||
"mijnhost": {
|
|
||||||
"credentials": "dns_mijn_host_api_key=0123456789abcdef0123456789abcdef",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-mijn-host",
|
|
||||||
"name": "mijn.host",
|
|
||||||
"package_name": "certbot-dns-mijn-host",
|
|
||||||
"version": "~=0.0.4"
|
|
||||||
},
|
|
||||||
"namecheap": {
|
|
||||||
"credentials": "dns_namecheap_username = 123456\ndns_namecheap_api_key = 0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-namecheap",
|
|
||||||
"name": "Namecheap",
|
|
||||||
"package_name": "certbot-dns-namecheap",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"namecom": {
|
|
||||||
"credentials": "dns_namecom_username = myusername\ndns_namecom_token = 0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-namecom",
|
|
||||||
"name": "Name.com",
|
|
||||||
"package_name": "certbot-dns-namecom",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"netcup": {
|
|
||||||
"credentials": "dns_netcup_customer_id = 123456\ndns_netcup_api_key = 0123456789abcdef0123456789abcdef01234567\ndns_netcup_api_password = abcdef0123456789abcdef01234567abcdef0123",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-netcup",
|
|
||||||
"name": "netcup",
|
|
||||||
"package_name": "certbot-dns-netcup",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"nicru": {
|
|
||||||
"credentials": "dns_nicru_client_id = application-id\ndns_nicru_client_secret = application-token\ndns_nicru_username = 0001110/NIC-D\ndns_nicru_password = password\ndns_nicru_scope = .+:.+/zones/example.com(/.+)?\ndns_nicru_service = DNS_SERVICE_NAME\ndns_nicru_zone = example.com",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-nicru",
|
|
||||||
"name": "nic.ru",
|
|
||||||
"package_name": "certbot-dns-nicru",
|
|
||||||
"version": "~=1.0.3"
|
|
||||||
},
|
|
||||||
"njalla": {
|
|
||||||
"credentials": "dns_njalla_token = 0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-njalla",
|
|
||||||
"name": "Njalla",
|
|
||||||
"package_name": "certbot-dns-njalla",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"nsone": {
|
|
||||||
"credentials": "dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-nsone",
|
|
||||||
"name": "NS1",
|
|
||||||
"package_name": "certbot-dns-nsone",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"oci": {
|
|
||||||
"credentials": "[DEFAULT]\nuser = ocid1.user.oc1...\nfingerprint = xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx\ntenancy = ocid1.tenancy.oc1...\nregion = us-ashburn-1\nkey_file = ~/.oci/oci_api_key.pem",
|
|
||||||
"dependencies": "oci",
|
|
||||||
"full_plugin_name": "dns-oci",
|
|
||||||
"name": "Oracle Cloud Infrastructure DNS",
|
|
||||||
"package_name": "certbot-dns-oci",
|
|
||||||
"version": "~=0.3.6"
|
|
||||||
},
|
|
||||||
"ovh": {
|
|
||||||
"credentials": "dns_ovh_endpoint = ovh-eu\ndns_ovh_application_key = MDAwMDAwMDAwMDAw\ndns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\ndns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-ovh",
|
|
||||||
"name": "OVH",
|
|
||||||
"package_name": "certbot-dns-ovh",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"plesk": {
|
|
||||||
"credentials": "dns_plesk_username = your-username\ndns_plesk_password = secret\ndns_plesk_api_url = https://plesk-api-host:8443",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-plesk",
|
|
||||||
"name": "Plesk",
|
|
||||||
"package_name": "certbot-dns-plesk",
|
|
||||||
"version": "~=0.3.0"
|
|
||||||
},
|
|
||||||
"porkbun": {
|
|
||||||
"credentials": "dns_porkbun_key=your-porkbun-api-key\ndns_porkbun_secret=your-porkbun-api-secret",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-porkbun",
|
|
||||||
"name": "Porkbun",
|
|
||||||
"package_name": "certbot-dns-porkbun",
|
|
||||||
"version": "~=0.11.0"
|
|
||||||
},
|
|
||||||
"powerdns": {
|
|
||||||
"credentials": "dns_powerdns_api_url = https://api.mypowerdns.example.org\ndns_powerdns_api_key = AbCbASsd!@34",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-powerdns",
|
|
||||||
"name": "PowerDNS",
|
|
||||||
"package_name": "certbot-dns-powerdns",
|
|
||||||
"version": "~=0.2.1"
|
|
||||||
},
|
|
||||||
"regru": {
|
|
||||||
"credentials": "dns_username=username\ndns_password=password",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns",
|
|
||||||
"name": "reg.ru",
|
|
||||||
"package_name": "certbot-regru",
|
|
||||||
"version": "~=1.0.2"
|
|
||||||
},
|
|
||||||
"rfc2136": {
|
|
||||||
"credentials": "# Target DNS server\ndns_rfc2136_server = 192.0.2.1\n# Target DNS port\ndns_rfc2136_port = 53\n# TSIG key name\ndns_rfc2136_name = keyname.\n# TSIG key secret\ndns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs AmKd7ak51vWKgSl12ib86oQRPkpDjg==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-rfc2136",
|
|
||||||
"name": "RFC 2136",
|
|
||||||
"package_name": "certbot-dns-rfc2136",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"rockenstein": {
|
|
||||||
"credentials": "dns_rockenstein_token=<token>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-rockenstein",
|
|
||||||
"name": "rockenstein AG",
|
|
||||||
"package_name": "certbot-dns-rockenstein",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
},
|
|
||||||
"route53": {
|
|
||||||
"credentials": "[default]\naws_access_key_id=AKIAIOSFODNN7EXAMPLE\naws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
||||||
"dependencies": "acme=={{certbot-version}}",
|
|
||||||
"full_plugin_name": "dns-route53",
|
|
||||||
"name": "Route 53 (Amazon)",
|
|
||||||
"package_name": "certbot-dns-route53",
|
|
||||||
"version": "=={{certbot-version}}"
|
|
||||||
},
|
|
||||||
"scaleway": {
|
|
||||||
"credentials": "dns_scaleway_application_token = b3a0b9a9-3814-4f12-95b0-a7473bf8b306",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-scaleway",
|
|
||||||
"name": "Scaleway",
|
|
||||||
"package_name": "certbot-dns-scaleway",
|
|
||||||
"version": "~=0.0.7"
|
|
||||||
},
|
|
||||||
"selectelv2": {
|
|
||||||
"credentials": "dns_selectel_api_v2_account_id = your_account_id\ndns_selectel_api_v2_project_name = your_project\ndns_selectel_api_v2_username = your_username\ndns_selectel_api_v2_password = your_password",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-selectel-api-v2",
|
|
||||||
"name": "Selectel api v2",
|
|
||||||
"package_name": "certbot-dns-selectel-api-v2",
|
|
||||||
"version": "~=0.3.0"
|
|
||||||
},
|
|
||||||
"simply": {
|
|
||||||
"credentials": "dns_simply_account_name = UExxxxxx\ndns_simply_api_key = DsHJdsjh2812872sahj",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-simply",
|
|
||||||
"name": "Simply",
|
|
||||||
"package_name": "certbot-dns-simply",
|
|
||||||
"version": "~=0.1.2"
|
|
||||||
},
|
|
||||||
"spaceship": {
|
|
||||||
"credentials": "[spaceship]\napi_key=your_api_key\napi_secret=your_api_secret",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-spaceship",
|
|
||||||
"name": "Spaceship",
|
|
||||||
"package_name": "certbot-dns-spaceship",
|
|
||||||
"version": "~=1.0.4"
|
|
||||||
},
|
|
||||||
"strato": {
|
|
||||||
"credentials": "dns_strato_username = user\ndns_strato_password = pass\n# uncomment if youre using two factor authentication:\n# dns_strato_totp_devicename = 2fa_device\n# dns_strato_totp_secret = 2fa_secret\n#\n# uncomment if domain name contains special characters\n# insert domain display name as seen on your account page here\n# dns_strato_domain_display_name = my-punicode-url.de\n#\n# if youre not using strato.de or another special endpoint you can customise it below\n# you will probably only need to adjust the host, but you can also change the complete endpoint url\n# dns_strato_custom_api_scheme = https\n# dns_strato_custom_api_host = www.strato.de\n# dns_strato_custom_api_port = 443\n# dns_strato_custom_api_path = \"/apps/CustomerService\"",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-strato",
|
|
||||||
"name": "Strato",
|
|
||||||
"package_name": "certbot-dns-strato",
|
|
||||||
"version": "~=0.2.2"
|
|
||||||
},
|
|
||||||
"tencentcloud": {
|
|
||||||
"credentials": "dns_tencentcloud_secret_id = TENCENT_CLOUD_SECRET_ID\ndns_tencentcloud_secret_key = TENCENT_CLOUD_SECRET_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-tencentcloud",
|
|
||||||
"name": "Tencent Cloud",
|
|
||||||
"package_name": "certbot-dns-tencentcloud",
|
|
||||||
"version": "~=2.1.1"
|
|
||||||
},
|
|
||||||
"timeweb": {
|
|
||||||
"credentials": "dns_timeweb_api_key = XXXXXXXXXXXXXXXXXXX",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-timeweb",
|
|
||||||
"name": "Timeweb Cloud",
|
|
||||||
"package_name": "certbot-dns-timeweb",
|
|
||||||
"version": "~=2.0.0"
|
|
||||||
},
|
|
||||||
"transip": {
|
|
||||||
"credentials": "dns_transip_username = my_username\ndns_transip_key_file = /etc/letsencrypt/transip-rsa.key",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-transip",
|
|
||||||
"name": "TransIP",
|
|
||||||
"package_name": "certbot-dns-transip",
|
|
||||||
"version": "~=0.5.2"
|
|
||||||
},
|
|
||||||
"vultr": {
|
|
||||||
"credentials": "dns_vultr_key = YOUR_VULTR_API_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-vultr",
|
|
||||||
"name": "Vultr",
|
|
||||||
"package_name": "certbot-dns-vultr",
|
|
||||||
"version": "~=1.1.0"
|
|
||||||
},
|
|
||||||
"websupport": {
|
|
||||||
"credentials": "dns_websupport_identifier = <api_key>\ndns_websupport_secret_key = <secret>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-websupport",
|
|
||||||
"name": "Websupport.sk",
|
|
||||||
"package_name": "certbot-dns-websupport",
|
|
||||||
"version": "~=5.0.0"
|
|
||||||
},
|
|
||||||
"wedos": {
|
|
||||||
"credentials": "dns_wedos_user = <wedos_registration>\ndns_wedos_auth = <wapi_password>",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-wedos",
|
|
||||||
"name": "Wedos",
|
|
||||||
"package_name": "certbot-dns-wedos",
|
|
||||||
"version": "~=2.2"
|
|
||||||
},
|
|
||||||
"zoneedit": {
|
|
||||||
"credentials": "dns_zoneedit_user = <login-user-id>\ndns_zoneedit_token = <dyn-authentication-token>",
|
|
||||||
"dependencies": "--no-deps dnspython",
|
|
||||||
"full_plugin_name": "dns-zoneedit",
|
|
||||||
"name": "ZoneEdit",
|
|
||||||
"package_name": "certbot-dns-zoneedit",
|
|
||||||
"version": "~=0.3.2"
|
|
||||||
},
|
|
||||||
"rcode0": {
|
|
||||||
"credentials": "dns_rcode0_api_key = acme_0123456789abcdef0123456789abcdef01234567",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-rcode0",
|
|
||||||
"name": "RcodeZero",
|
|
||||||
"package_name": "certbot-dns-rcode0",
|
|
||||||
"version": "~=0.0.0.2"
|
|
||||||
},
|
|
||||||
"lws": {
|
|
||||||
"credentials": "dns_lws_login = 123456\ndns_lws_api_key = YOUR_API_KEY",
|
|
||||||
"dependencies": "",
|
|
||||||
"full_plugin_name": "dns-lws",
|
|
||||||
"name": "LWS",
|
|
||||||
"package_name": "certbot-dns-lws",
|
|
||||||
"version": "~=1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"database": {
|
"database": {
|
||||||
"engine": "mysql2",
|
"engine": "mysql",
|
||||||
"host": "db",
|
"host": "db",
|
||||||
"name": "npm",
|
"name": "npm",
|
||||||
"user": "npm",
|
"user": "npm",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"database": {
|
"database": {
|
||||||
"engine": "knex-native",
|
"engine": "knex-native",
|
||||||
"knex": {
|
"knex": {
|
||||||
"client": "better-sqlite3",
|
"client": "sqlite3",
|
||||||
"connection": {
|
"connection": {
|
||||||
"filename": "/app/config/mydb.sqlite"
|
"filename": "/app/config/mydb.sqlite"
|
||||||
},
|
},
|
||||||
|
|||||||
+27
-36
@@ -1,42 +1,33 @@
|
|||||||
import knex from "knex";
|
const config = require('config');
|
||||||
import { configGet, configHas } from "./lib/config.js";
|
|
||||||
|
|
||||||
let instance = null;
|
if (!config.has('database')) {
|
||||||
|
throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md');
|
||||||
|
}
|
||||||
|
|
||||||
const generateDbConfig = () => {
|
function generateDbConfig() {
|
||||||
if (!configHas("database")) {
|
if (config.database.engine === 'knex-native') {
|
||||||
throw new Error(
|
return config.database.knex;
|
||||||
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
|
} else
|
||||||
);
|
return {
|
||||||
}
|
client: config.database.engine,
|
||||||
|
connection: {
|
||||||
|
host: config.database.host,
|
||||||
|
user: config.database.user,
|
||||||
|
password: config.database.password,
|
||||||
|
database: config.database.name,
|
||||||
|
port: config.database.port
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
tableName: 'migrations'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const cfg = configGet("database");
|
|
||||||
|
|
||||||
if (cfg.engine === "knex-native") {
|
let data = generateDbConfig();
|
||||||
return cfg.knex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (typeof config.database.version !== 'undefined') {
|
||||||
client: cfg.engine,
|
data.version = config.database.version;
|
||||||
connection: {
|
}
|
||||||
host: cfg.host,
|
|
||||||
user: cfg.user,
|
|
||||||
password: cfg.password,
|
|
||||||
database: cfg.name,
|
|
||||||
port: cfg.port,
|
|
||||||
...(cfg.ssl ? { ssl: cfg.ssl } : {}),
|
|
||||||
},
|
|
||||||
migrations: {
|
|
||||||
tableName: "migrations",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInstance = () => {
|
module.exports = require('knex')(data);
|
||||||
if (!instance) {
|
|
||||||
instance = knex(generateDbConfig());
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getInstance;
|
|
||||||
File diff suppressed because it is too large.
Load diff
+106
-28
@@ -1,56 +1,134 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import app from "./app.js";
|
const logger = require('./logger').global;
|
||||||
import internalCertificate from "./internal/certificate.js";
|
|
||||||
import internalIpRanges from "./internal/ip_ranges.js";
|
|
||||||
import { global as logger } from "./logger.js";
|
|
||||||
import { migrateUp } from "./migrate.js";
|
|
||||||
import { getCompiledSchema } from "./schema/index.js";
|
|
||||||
import setup from "./setup.js";
|
|
||||||
|
|
||||||
const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false";
|
async function appStart () {
|
||||||
|
// Create config file db settings if environment variables have been set
|
||||||
|
await createDbConfigFromEnvironment();
|
||||||
|
|
||||||
async function appStart() {
|
const migrate = require('./migrate');
|
||||||
return migrateUp()
|
const setup = require('./setup');
|
||||||
|
const app = require('./app');
|
||||||
|
const apiValidator = require('./lib/validator/api');
|
||||||
|
const internalCertificate = require('./internal/certificate');
|
||||||
|
const internalIpRanges = require('./internal/ip_ranges');
|
||||||
|
|
||||||
|
return migrate.latest()
|
||||||
.then(setup)
|
.then(setup)
|
||||||
.then(getCompiledSchema)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!IP_RANGES_FETCH_ENABLED) {
|
return apiValidator.loadSchemas;
|
||||||
logger.info("IP Ranges fetch is disabled by environment variable");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info("IP Ranges fetch is enabled");
|
|
||||||
return internalIpRanges.fetch().catch((err) => {
|
|
||||||
logger.error("IP Ranges fetch failed, continuing anyway:", err.message);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
.then(internalIpRanges.fetch)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
internalCertificate.initTimer();
|
internalCertificate.initTimer();
|
||||||
if (IP_RANGES_FETCH_ENABLED) {
|
internalIpRanges.initTimer();
|
||||||
internalIpRanges.initTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = app.listen(3000, () => {
|
const server = app.listen(3000, () => {
|
||||||
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
|
logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
|
||||||
|
|
||||||
process.on("SIGTERM", () => {
|
process.on('SIGTERM', () => {
|
||||||
logger.info(`PID ${process.pid} received SIGTERM`);
|
logger.info('PID ' + process.pid + ' received SIGTERM');
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
logger.info("Stopping.");
|
logger.info('Stopping.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error(`Startup Error: ${err.message}`, err);
|
logger.error(err.message);
|
||||||
setTimeout(appStart, 1000);
|
setTimeout(appStart, 1000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createDbConfigFromEnvironment() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
|
||||||
|
const envMysqlPort = process.env.DB_MYSQL_PORT || null;
|
||||||
|
const envMysqlUser = process.env.DB_MYSQL_USER || null;
|
||||||
|
const envMysqlName = process.env.DB_MYSQL_NAME || null;
|
||||||
|
const envSqliteFile = process.env.DB_SQLITE_FILE || null;
|
||||||
|
|
||||||
|
if ((envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) || envSqliteFile) {
|
||||||
|
const fs = require('fs');
|
||||||
|
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
|
||||||
|
let configData = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
configData = require(filename);
|
||||||
|
} catch (err) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configData.database && configData.database.engine && !configData.database.fromEnv) {
|
||||||
|
logger.info('Manual db configuration already exists, skipping config creation from environment variables');
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) {
|
||||||
|
const newConfig = {
|
||||||
|
fromEnv: true,
|
||||||
|
engine: 'mysql',
|
||||||
|
host: envMysqlHost,
|
||||||
|
port: envMysqlPort,
|
||||||
|
user: envMysqlUser,
|
||||||
|
password: process.env.DB_MYSQL_PASSWORD,
|
||||||
|
name: envMysqlName,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
|
||||||
|
// Config is unchanged, skip overwrite
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Generating MySQL db configuration from environment variables');
|
||||||
|
configData.database = newConfig;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const newConfig = {
|
||||||
|
fromEnv: true,
|
||||||
|
engine: 'knex-native',
|
||||||
|
knex: {
|
||||||
|
client: 'sqlite3',
|
||||||
|
connection: {
|
||||||
|
filename: envSqliteFile
|
||||||
|
},
|
||||||
|
useNullAsDefault: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
|
||||||
|
// Config is unchanged, skip overwrite
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Generating Sqlite db configuration from environment variables');
|
||||||
|
configData.database = newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write config
|
||||||
|
fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Could not write db config to config file: ' + filename);
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
logger.info('Wrote db configuration to config file: ' + filename);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appStart();
|
appStart();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.fatal(err);
|
logger.error(err.message, err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import { createGuardrails, generateSecret, generateURI, verify } from "otplib";
|
|
||||||
import errs from "../lib/error.js";
|
|
||||||
import authModel from "../models/auth.js";
|
|
||||||
import internalUser from "./user.js";
|
|
||||||
|
|
||||||
const APP_NAME = "Nginx Proxy Manager";
|
|
||||||
const BACKUP_CODE_COUNT = 8;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate backup codes
|
|
||||||
* @returns {Promise<{plain: string[], hashed: string[]}>}
|
|
||||||
*/
|
|
||||||
const generateBackupCodes = async () => {
|
|
||||||
const plain = [];
|
|
||||||
const hashed = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < BACKUP_CODE_COUNT; i++) {
|
|
||||||
const code = crypto.randomBytes(4).toString("hex").toUpperCase();
|
|
||||||
plain.push(code);
|
|
||||||
const hash = await bcrypt.hash(code, 10);
|
|
||||||
hashed.push(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { plain, hashed };
|
|
||||||
};
|
|
||||||
|
|
||||||
const internal2fa = {
|
|
||||||
/**
|
|
||||||
* Check if user has 2FA enabled
|
|
||||||
* @param {number} userId
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
isEnabled: async (userId) => {
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
return auth?.meta?.totp_enabled === true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get 2FA status for user
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {number} userId
|
|
||||||
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
|
|
||||||
*/
|
|
||||||
getStatus: async (access, userId) => {
|
|
||||||
await access.can("users:password", userId);
|
|
||||||
await internalUser.get(access, { id: userId });
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
const enabled = auth?.meta?.totp_enabled === true;
|
|
||||||
let backup_codes_remaining = 0;
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
const backupCodes = auth.meta.backup_codes || [];
|
|
||||||
backup_codes_remaining = backupCodes.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled,
|
|
||||||
backup_codes_remaining,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start 2FA setup - store pending secret
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {number} userId
|
|
||||||
* @returns {Promise<{secret: string, otpauth_url: string}>}
|
|
||||||
*/
|
|
||||||
startSetup: async (access, userId) => {
|
|
||||||
await access.can("users:password", userId);
|
|
||||||
const user = await internalUser.get(access, { id: userId });
|
|
||||||
const secret = generateSecret();
|
|
||||||
const otpauth_url = generateURI({
|
|
||||||
issuer: APP_NAME,
|
|
||||||
label: user.email,
|
|
||||||
secret: secret,
|
|
||||||
});
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
|
|
||||||
// ensure user isn't already setup for 2fa
|
|
||||||
const enabled = auth?.meta?.totp_enabled === true;
|
|
||||||
if (enabled) {
|
|
||||||
throw new errs.ValidationError("2FA is already enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = auth.meta || {};
|
|
||||||
meta.totp_pending_secret = secret;
|
|
||||||
|
|
||||||
await authModel
|
|
||||||
.query()
|
|
||||||
.where("id", auth.id)
|
|
||||||
.andWhere("user_id", userId)
|
|
||||||
.andWhere("type", "password")
|
|
||||||
.patch({ meta });
|
|
||||||
|
|
||||||
return { secret, otpauth_url };
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable 2FA after verifying code
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {number} userId
|
|
||||||
* @param {string} code
|
|
||||||
* @returns {Promise<{backup_codes: string[]}>}
|
|
||||||
*/
|
|
||||||
enable: async (access, userId, code) => {
|
|
||||||
await access.can("users:password", userId);
|
|
||||||
await internalUser.get(access, { id: userId });
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
const secret = auth?.meta?.totp_pending_secret || false;
|
|
||||||
|
|
||||||
if (!secret) {
|
|
||||||
throw new errs.ValidationError("No pending 2FA setup found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await verify({ token: code, secret });
|
|
||||||
if (!result.valid) {
|
|
||||||
throw new errs.ValidationError("Invalid verification code");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { plain, hashed } = await generateBackupCodes();
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
...auth.meta,
|
|
||||||
totp_secret: secret,
|
|
||||||
totp_enabled: true,
|
|
||||||
totp_enabled_at: new Date().toISOString(),
|
|
||||||
backup_codes: hashed,
|
|
||||||
};
|
|
||||||
delete meta.totp_pending_secret;
|
|
||||||
|
|
||||||
await authModel
|
|
||||||
.query()
|
|
||||||
.where("id", auth.id)
|
|
||||||
.andWhere("user_id", userId)
|
|
||||||
.andWhere("type", "password")
|
|
||||||
.patch({ meta });
|
|
||||||
|
|
||||||
return { backup_codes: plain };
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable 2FA
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {number} userId
|
|
||||||
* @param {string} code
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
disable: async (access, userId, code) => {
|
|
||||||
await access.can("users:password", userId);
|
|
||||||
await internalUser.get(access, { id: userId });
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
|
|
||||||
const enabled = auth?.meta?.totp_enabled === true;
|
|
||||||
if (!enabled) {
|
|
||||||
throw new errs.ValidationError("2FA is not enabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await verify({
|
|
||||||
token: code,
|
|
||||||
secret: auth.meta.totp_secret,
|
|
||||||
guardrails: createGuardrails({
|
|
||||||
MIN_SECRET_BYTES: 10,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.valid) {
|
|
||||||
throw new errs.AuthError("Invalid verification code");
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = { ...auth.meta };
|
|
||||||
delete meta.totp_secret;
|
|
||||||
delete meta.totp_enabled;
|
|
||||||
delete meta.totp_enabled_at;
|
|
||||||
delete meta.backup_codes;
|
|
||||||
|
|
||||||
await authModel
|
|
||||||
.query()
|
|
||||||
.where("id", auth.id)
|
|
||||||
.andWhere("user_id", userId)
|
|
||||||
.andWhere("type", "password")
|
|
||||||
.patch({ meta });
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify 2FA code for login
|
|
||||||
*
|
|
||||||
* @param {number} userId
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
verifyForLogin: async (userId, token) => {
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
const secret = auth?.meta?.totp_secret || false;
|
|
||||||
|
|
||||||
if (!secret) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars
|
|
||||||
// and the backup codes are 8 chars.
|
|
||||||
if (token.length === 6) {
|
|
||||||
const result = await verify({
|
|
||||||
token,
|
|
||||||
secret,
|
|
||||||
// These guardrails lower the minimum length requirement for secrets.
|
|
||||||
// In v12 of otplib the default minimum length is 10 and in v13 it is 16.
|
|
||||||
// Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets
|
|
||||||
// so people won't be locked out when upgrading.
|
|
||||||
guardrails: createGuardrails({
|
|
||||||
MIN_SECRET_BYTES: 10,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.valid) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try backup codes
|
|
||||||
const backupCodes = auth?.meta?.backup_codes || [];
|
|
||||||
for (let i = 0; i < backupCodes.length; i++) {
|
|
||||||
const match = await bcrypt.compare(token.toUpperCase(), backupCodes[i]);
|
|
||||||
if (match) {
|
|
||||||
// Remove used backup code
|
|
||||||
const updatedCodes = [...backupCodes];
|
|
||||||
updatedCodes.splice(i, 1);
|
|
||||||
const meta = { ...auth.meta, backup_codes: updatedCodes };
|
|
||||||
await authModel
|
|
||||||
.query()
|
|
||||||
.where("id", auth.id)
|
|
||||||
.andWhere("user_id", userId)
|
|
||||||
.andWhere("type", "password")
|
|
||||||
.patch({ meta });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regenerate backup codes
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {number} userId
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<{backup_codes: string[]}>}
|
|
||||||
*/
|
|
||||||
regenerateBackupCodes: async (access, userId, token) => {
|
|
||||||
await access.can("users:password", userId);
|
|
||||||
await internalUser.get(access, { id: userId });
|
|
||||||
const auth = await internal2fa.getUserPasswordAuth(userId);
|
|
||||||
const enabled = auth?.meta?.totp_enabled === true;
|
|
||||||
const secret = auth?.meta?.totp_secret || false;
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
throw new errs.ValidationError("2FA is not enabled");
|
|
||||||
}
|
|
||||||
if (!secret) {
|
|
||||||
throw new errs.ValidationError("No 2FA secret found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await verify({
|
|
||||||
token,
|
|
||||||
secret,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.valid) {
|
|
||||||
throw new errs.ValidationError("Invalid verification code");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { plain, hashed } = await generateBackupCodes();
|
|
||||||
|
|
||||||
const meta = { ...auth.meta, backup_codes: hashed };
|
|
||||||
await authModel
|
|
||||||
.query()
|
|
||||||
.where("id", auth.id)
|
|
||||||
.andWhere("user_id", userId)
|
|
||||||
.andWhere("type", "password")
|
|
||||||
.patch({ meta });
|
|
||||||
|
|
||||||
return { backup_codes: plain };
|
|
||||||
},
|
|
||||||
|
|
||||||
getUserPasswordAuth: async (userId) => {
|
|
||||||
const auth = await authModel.query().where("user_id", userId).andWhere("type", "password").first();
|
|
||||||
|
|
||||||
if (!auth) {
|
|
||||||
throw new errs.ItemNotFoundError("Auth not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return auth;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default internal2fa;
|
|
||||||
+414
-422
@@ -1,124 +1,103 @@
|
|||||||
import fs from "node:fs";
|
const _ = require('lodash');
|
||||||
import batchflow from "batchflow";
|
const fs = require('fs');
|
||||||
import _ from "lodash";
|
const batchflow = require('batchflow');
|
||||||
import errs from "../lib/error.js";
|
const logger = require('../logger').access;
|
||||||
import { isValid, unsafeValueError } from "../lib/nginx-values.js";
|
const error = require('../lib/error');
|
||||||
import { isMysql, isPostgres } from "../lib/config.js";
|
const accessListModel = require('../models/access_list');
|
||||||
import utils from "../lib/utils.js";
|
const accessListAuthModel = require('../models/access_list_auth');
|
||||||
import db from "../db.js";
|
const accessListClientModel = require('../models/access_list_client');
|
||||||
import { access as logger } from "../logger.js";
|
const proxyHostModel = require('../models/proxy_host');
|
||||||
import accessListModel from "../models/access_list.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import accessListAuthModel from "../models/access_list_auth.js";
|
const internalNginx = require('./nginx');
|
||||||
import accessListClientModel from "../models/access_list_client.js";
|
const utils = require('../lib/utils');
|
||||||
import proxyHostModel from "../models/proxy_host.js";
|
|
||||||
import internalAuditLog from "./audit-log.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find proxy hosts that reference an access list in their locations JSON.
|
|
||||||
*
|
|
||||||
* @param {Integer} accessListId
|
|
||||||
* @returns {Promise<Array>}
|
|
||||||
*/
|
|
||||||
const getProxyHostsUsingAccessListInLocations = async (accessListId) => {
|
|
||||||
let result;
|
|
||||||
if (isMysql()) {
|
|
||||||
const searchObj = JSON.stringify([{ access_list_id: accessListId }]);
|
|
||||||
result = await db().raw("SELECT id FROM proxy_host WHERE is_deleted = 0 AND JSON_CONTAINS(locations, ?, ?)", [
|
|
||||||
searchObj,
|
|
||||||
"$",
|
|
||||||
]);
|
|
||||||
} else if (isPostgres()) {
|
|
||||||
result = await db().raw("SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations::jsonb @> ?::jsonb", [
|
|
||||||
JSON.stringify([{ access_list_id: accessListId }]),
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
result = await db().raw("SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations LIKE ?", [
|
|
||||||
`%"access_list_id":${accessListId}%`,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
// knex raw() returns [rows, metadata] for MySQL, { rows } for Postgres and rows for SQLite
|
|
||||||
if (!Array.isArray(result)) {
|
|
||||||
return result?.rows || [];
|
|
||||||
}
|
|
||||||
return (Array.isArray(result[0]) ? result[0] : result) || [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const internalAccessList = {
|
const internalAccessList = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: async (access, data) => {
|
create: (access, data) => {
|
||||||
await access.can("access_lists:create", data);
|
return access.can('access_lists:create', data)
|
||||||
const row = await accessListModel
|
.then((/*access_data*/) => {
|
||||||
.query()
|
return accessListModel
|
||||||
.insertAndFetch({
|
.query()
|
||||||
name: data.name,
|
.omit(omissions())
|
||||||
satisfy_any: data.satisfy_any,
|
.insertAndFetch({
|
||||||
pass_auth: data.pass_auth,
|
name: data.name,
|
||||||
owner_user_id: access.token.getUserId(1),
|
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;
|
||||||
|
|
||||||
data.id = row.id;
|
let promises = [];
|
||||||
|
|
||||||
const promises = [];
|
// Now add the items
|
||||||
// Items
|
data.items.map((item) => {
|
||||||
data.items.map((item) => {
|
promises.push(accessListAuthModel
|
||||||
promises.push(
|
.query()
|
||||||
accessListAuthModel.query().insert({
|
.insert({
|
||||||
access_list_id: row.id,
|
access_list_id: row.id,
|
||||||
username: item.username,
|
username: item.username,
|
||||||
password: item.password,
|
password: item.password
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
return true;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
// 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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Clients
|
return Promise.all(promises);
|
||||||
for (const client of data.clients ?? []) {
|
})
|
||||||
await accessListClientModel.query().insert({
|
.then(() => {
|
||||||
access_list_id: row.id,
|
// re-fetch with expansions
|
||||||
address: client.address,
|
return internalAccessList.get(access, {
|
||||||
directive: client.directive,
|
id: data.id,
|
||||||
|
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
|
||||||
|
}, true /* <- skip masking */);
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
// Audit log
|
||||||
|
data.meta = _.assign({}, data.meta || {}, row.meta);
|
||||||
|
|
||||||
|
return internalAccessList.build(row)
|
||||||
|
.then(() => {
|
||||||
|
if (row.proxy_host_count) {
|
||||||
|
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'created',
|
||||||
|
object_type: 'access-list',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: internalAccessList.maskItems(data)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return internalAccessList.maskItems(row);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// re-fetch with expansions
|
|
||||||
const freshRow = await internalAccessList.get(
|
|
||||||
access,
|
|
||||||
{
|
|
||||||
id: data.id,
|
|
||||||
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
|
|
||||||
},
|
|
||||||
true, // skip masking
|
|
||||||
);
|
|
||||||
|
|
||||||
// Audit log
|
|
||||||
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
|
|
||||||
await internalAccessList.build(freshRow);
|
|
||||||
|
|
||||||
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
|
|
||||||
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to audit log
|
|
||||||
await internalAuditLog.add(access, {
|
|
||||||
action: "created",
|
|
||||||
object_type: "access-list",
|
|
||||||
object_id: freshRow.id,
|
|
||||||
meta: internalAccessList.maskItems(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
return internalAccessList.maskItems(freshRow);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,120 +108,130 @@ const internalAccessList = {
|
|||||||
* @param {String} [data.items]
|
* @param {String} [data.items]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: async (access, data) => {
|
update: (access, data) => {
|
||||||
await access.can("access_lists:update", data.id);
|
return access.can('access_lists:update', data.id)
|
||||||
const row = await internalAccessList.get(access, { id: data.id });
|
.then((/*access_data*/) => {
|
||||||
if (row.id !== data.id) {
|
return internalAccessList.get(access, {id: data.id});
|
||||||
// Sanity check that something crazy hasn't happened
|
})
|
||||||
throw new errs.InternalValidationError(
|
.then((row) => {
|
||||||
`Access List could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
|
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);
|
||||||
|
|
||||||
// patch name if specified
|
|
||||||
if (typeof data.name !== "undefined" && data.name) {
|
|
||||||
await accessListModel.query().where({ id: data.id }).patch({
|
|
||||||
name: data.name,
|
|
||||||
satisfy_any: data.satisfy_any,
|
|
||||||
pass_auth: data.pass_auth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for items and add/update/remove them
|
|
||||||
if (typeof data.items !== "undefined" && data.items) {
|
|
||||||
const promises = [];
|
|
||||||
const itemsToKeep = [];
|
|
||||||
|
|
||||||
data.items.map((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
|
|
||||||
itemsToKeep.push(item.username);
|
|
||||||
}
|
}
|
||||||
return true;
|
})
|
||||||
});
|
.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 = [];
|
||||||
|
|
||||||
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
|
data.items.map(function (item) {
|
||||||
|
if (item.password) {
|
||||||
if (itemsToKeep.length) {
|
promises.push(accessListAuthModel
|
||||||
query.andWhere("username", "NOT IN", itemsToKeep);
|
.query()
|
||||||
}
|
.insert({
|
||||||
|
access_list_id: data.id,
|
||||||
await query;
|
username: item.username,
|
||||||
// Add new items
|
password: item.password
|
||||||
if (promises.length) {
|
})
|
||||||
await Promise.all(promises);
|
);
|
||||||
}
|
} else {
|
||||||
}
|
// This was supplied with an empty password, which means keep it but don't change the password
|
||||||
|
items_to_keep.push(item.username);
|
||||||
// Check for clients and add/update/remove them
|
}
|
||||||
if (typeof data.clients !== "undefined" && data.clients) {
|
|
||||||
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
|
|
||||||
await query;
|
|
||||||
|
|
||||||
for (const client of data.clients) {
|
|
||||||
if (client.address) {
|
|
||||||
await accessListClientModel.query().insert({
|
|
||||||
access_list_id: data.id,
|
|
||||||
address: client.address,
|
|
||||||
directive: client.directive,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
|
||||||
// Add to audit log
|
data.clients.map(function (client) {
|
||||||
await internalAuditLog.add(access, {
|
if (client.address) {
|
||||||
action: "updated",
|
promises.push(accessListClientModel
|
||||||
object_type: "access-list",
|
.query()
|
||||||
object_id: data.id,
|
.insert({
|
||||||
meta: internalAccessList.maskItems(data),
|
access_list_id: data.id,
|
||||||
});
|
address: client.address,
|
||||||
|
directive: client.directive
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// re-fetch with expansions
|
let query = accessListClientModel
|
||||||
const freshRow = await internalAccessList.get(
|
.query()
|
||||||
access,
|
.delete()
|
||||||
{
|
.where('access_list_id', data.id);
|
||||||
id: data.id,
|
|
||||||
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
|
|
||||||
},
|
|
||||||
true, // skip masking
|
|
||||||
);
|
|
||||||
|
|
||||||
await internalAccessList.build(freshRow);
|
return query
|
||||||
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
|
.then(() => {
|
||||||
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
|
// Add new items
|
||||||
}
|
if (promises.length) {
|
||||||
|
return Promise.all(promises);
|
||||||
// Also regenerate configs for proxy hosts that reference this access list in their locations
|
}
|
||||||
const locationHostRows = await getProxyHostsUsingAccessListInLocations(data.id);
|
});
|
||||||
if (locationHostRows?.length) {
|
}
|
||||||
const locationHostIds = locationHostRows
|
})
|
||||||
.map((r) => r.id)
|
.then(internalNginx.reload)
|
||||||
.filter((id) => {
|
.then(() => {
|
||||||
// Exclude hosts already regenerated above
|
// Add to audit log
|
||||||
return !freshRow.proxy_hosts?.find((h) => h.id === id);
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'updated',
|
||||||
|
object_type: 'access-list',
|
||||||
|
object_id: data.id,
|
||||||
|
meta: internalAccessList.maskItems(data)
|
||||||
});
|
});
|
||||||
if (locationHostIds.length) {
|
})
|
||||||
const locationHosts = await proxyHostModel
|
.then(() => {
|
||||||
.query()
|
// re-fetch with expansions
|
||||||
.where("is_deleted", 0)
|
return internalAccessList.get(access, {
|
||||||
.whereIn("id", locationHostIds)
|
id: data.id,
|
||||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
|
||||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
}, true /* <- skip masking */);
|
||||||
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
|
})
|
||||||
}
|
.then((row) => {
|
||||||
}
|
return internalAccessList.build(row)
|
||||||
|
.then(() => {
|
||||||
await internalNginx.reload();
|
if (row.proxy_host_count) {
|
||||||
return internalAccessList.maskItems(freshRow);
|
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return internalAccessList.maskItems(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,46 +240,52 @@ const internalAccessList = {
|
|||||||
* @param {Integer} data.id
|
* @param {Integer} data.id
|
||||||
* @param {Array} [data.expand]
|
* @param {Array} [data.expand]
|
||||||
* @param {Array} [data.omit]
|
* @param {Array} [data.omit]
|
||||||
* @param {Boolean} [skipMasking]
|
* @param {Boolean} [skip_masking]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: async (access, data, skipMasking) => {
|
get: (access, data, skip_masking) => {
|
||||||
const thisData = data || {};
|
if (typeof data === 'undefined') {
|
||||||
const accessData = await access.can("access_lists:get", thisData.id);
|
data = {};
|
||||||
|
}
|
||||||
|
|
||||||
const query = accessListModel
|
return access.can('access_lists:get', data.id)
|
||||||
.query()
|
.then((access_data) => {
|
||||||
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
|
let query = accessListModel
|
||||||
.leftJoin("proxy_host", function () {
|
.query()
|
||||||
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn("proxy_host.is_deleted", "=", 0);
|
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
|
||||||
|
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
|
||||||
|
.where('access_list.is_deleted', 0)
|
||||||
|
.andWhere('access_list.id', data.id)
|
||||||
|
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
|
||||||
|
.omit(['access_list.is_deleted'])
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (access_data.permission_visibility !== 'all') {
|
||||||
|
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom omissions
|
||||||
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
})
|
})
|
||||||
.where("access_list.is_deleted", 0)
|
.then((row) => {
|
||||||
.andWhere("access_list.id", thisData.id)
|
if (row) {
|
||||||
.groupBy("access_list.id")
|
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
|
||||||
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
|
row = internalAccessList.maskItems(row);
|
||||||
.first();
|
}
|
||||||
|
|
||||||
if (accessData.permission_visibility !== "all") {
|
return _.omit(row, omissions());
|
||||||
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
|
} else {
|
||||||
}
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
});
|
||||||
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let row = await query.then(utils.omitRow(omissions()));
|
|
||||||
|
|
||||||
if (!row?.id) {
|
|
||||||
throw new errs.ItemNotFoundError(thisData.id);
|
|
||||||
}
|
|
||||||
if (!skipMasking && 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;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,77 +295,73 @@ const internalAccessList = {
|
|||||||
* @param {String} [data.reason]
|
* @param {String} [data.reason]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: async (access, data) => {
|
delete: (access, data) => {
|
||||||
await access.can("access_lists:delete", data.id);
|
return access.can('access_lists:delete', data.id)
|
||||||
const row = await internalAccessList.get(access, {
|
.then(() => {
|
||||||
id: data.id,
|
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
|
||||||
expand: ["proxy_hosts", "items", "clients"],
|
})
|
||||||
});
|
.then((row) => {
|
||||||
|
if (!row) {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (!row?.id) {
|
// 1. update row to be deleted
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
// 2. update any proxy hosts that were using it (ignoring permissions)
|
||||||
}
|
// 3. reconfigure those hosts
|
||||||
|
// 4. audit log
|
||||||
|
|
||||||
// 1. update row to be deleted
|
// 1. update row to be deleted
|
||||||
// 2. update any proxy hosts that were using it (ignoring permissions)
|
return accessListModel
|
||||||
// 3. reconfigure those hosts
|
.query()
|
||||||
// 4. audit log
|
.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
|
||||||
|
|
||||||
// 1. update row to be deleted
|
// set the access_list_id to zero for these items
|
||||||
await accessListModel.query().where("id", row.id).patch({
|
row.proxy_hosts.map(function (val, idx) {
|
||||||
is_deleted: 1,
|
row.proxy_hosts[idx].access_list_id = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. update any proxy hosts that were using it (ignoring permissions)
|
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
|
||||||
const affectedHostIds = new Set((row.proxy_hosts || []).map((h) => h.id));
|
})
|
||||||
if (affectedHostIds.size) {
|
.then(() => {
|
||||||
await proxyHostModel.query().where("access_list_id", "=", row.id).patch({ access_list_id: 0 });
|
return internalNginx.reload();
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// delete the htpasswd file
|
||||||
|
let htpasswd_file = internalAccessList.getFilename(row);
|
||||||
|
|
||||||
// Also clear it from any proxy host locations using it, these will then inherit the host's access list
|
try {
|
||||||
const locationHostRows = await getProxyHostsUsingAccessListInLocations(row.id);
|
fs.unlinkSync(htpasswd_file);
|
||||||
for (const { id: hostId } of locationHostRows) {
|
} catch (err) {
|
||||||
const host = await proxyHostModel.query().where("id", hostId).first();
|
// do nothing
|
||||||
if (host?.locations?.some((loc) => loc.access_list_id === row.id)) {
|
}
|
||||||
const updatedLocations = host.locations.map((loc) => {
|
})
|
||||||
if (loc.access_list_id === row.id) {
|
.then(() => {
|
||||||
return { ...loc, access_list_id: 0 };
|
// 4. audit log
|
||||||
}
|
return internalAuditLog.add(access, {
|
||||||
return loc;
|
action: 'deleted',
|
||||||
});
|
object_type: 'access-list',
|
||||||
await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations });
|
object_id: row.id,
|
||||||
affectedHostIds.add(hostId);
|
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
|
||||||
}
|
});
|
||||||
}
|
});
|
||||||
|
})
|
||||||
// 3. reconfigure those hosts from fresh rows, then reload nginx
|
.then(() => {
|
||||||
if (affectedHostIds.size) {
|
return true;
|
||||||
const affectedHosts = await proxyHostModel
|
});
|
||||||
.query()
|
|
||||||
.where("is_deleted", 0)
|
|
||||||
.whereIn("id", [...affectedHostIds])
|
|
||||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
|
||||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
|
||||||
await internalNginx.bulkGenerateConfigs("proxy_host", affectedHosts);
|
|
||||||
}
|
|
||||||
|
|
||||||
await internalNginx.reload();
|
|
||||||
|
|
||||||
// delete the htpasswd file
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(internalAccessList.getFilename(row));
|
|
||||||
} catch (_err) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. audit log
|
|
||||||
await internalAuditLog.add(access, {
|
|
||||||
action: "deleted",
|
|
||||||
object_type: "access-list",
|
|
||||||
object_id: row.id,
|
|
||||||
meta: _.omit(internalAccessList.maskItems(row), ["is_deleted", "proxy_hosts"]),
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,66 +369,73 @@ const internalAccessList = {
|
|||||||
*
|
*
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Array} [expand]
|
* @param {Array} [expand]
|
||||||
* @param {String} [searchQuery]
|
* @param {String} [search_query]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: async (access, expand, searchQuery) => {
|
getAll: (access, expand, search_query) => {
|
||||||
const accessData = await access.can("access_lists:list");
|
return access.can('access_lists:list')
|
||||||
|
.then((access_data) => {
|
||||||
|
let query = accessListModel
|
||||||
|
.query()
|
||||||
|
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
|
||||||
|
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
|
||||||
|
.where('access_list.is_deleted', 0)
|
||||||
|
.groupBy('access_list.id')
|
||||||
|
.omit(['access_list.is_deleted'])
|
||||||
|
.allowEager('[owner,items,clients]')
|
||||||
|
.orderBy('access_list.name', 'ASC');
|
||||||
|
|
||||||
const query = accessListModel
|
if (access_data.permission_visibility !== 'all') {
|
||||||
.query()
|
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
|
||||||
.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 (accessData.permission_visibility !== "all") {
|
|
||||||
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query is used for searching
|
|
||||||
if (typeof searchQuery === "string") {
|
|
||||||
query.where(function () {
|
|
||||||
this.where("name", "like", `%${searchQuery}%`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await query.then(utils.omitRows(omissions()));
|
|
||||||
if (rows) {
|
|
||||||
rows.map((row, idx) => {
|
|
||||||
if (typeof row.items !== "undefined" && row.items) {
|
|
||||||
rows[idx] = internalAccessList.maskItems(row);
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
|
// Query is used for searching
|
||||||
|
if (typeof search_query === 'string') {
|
||||||
|
query.where(function () {
|
||||||
|
this.where('name', 'like', '%' + search_query + '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then((rows) => {
|
||||||
|
if (rows) {
|
||||||
|
rows.map(function (row, idx) {
|
||||||
|
if (typeof row.items !== 'undefined' && row.items) {
|
||||||
|
rows[idx] = internalAccessList.maskItems(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count is used in reports
|
* Report use
|
||||||
*
|
*
|
||||||
* @param {Integer} userId
|
* @param {Integer} user_id
|
||||||
* @param {String} visibility
|
* @param {String} visibility
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getCount: async (userId, visibility) => {
|
getCount: (user_id, visibility) => {
|
||||||
const query = accessListModel.query().count("id as count").where("is_deleted", 0);
|
let query = accessListModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0);
|
||||||
|
|
||||||
if (visibility !== "all") {
|
if (visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", userId);
|
query.andWhere('owner_user_id', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await query.first();
|
return query.first()
|
||||||
return Number.parseInt(row.count, 10);
|
.then((row) => {
|
||||||
|
return parseInt(row.count, 10);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -445,21 +443,21 @@ const internalAccessList = {
|
|||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
maskItems: (list) => {
|
maskItems: (list) => {
|
||||||
if (list && typeof list.items !== "undefined") {
|
if (list && typeof list.items !== 'undefined') {
|
||||||
list.items.map((val, idx) => {
|
list.items.map(function (val, idx) {
|
||||||
let repeatFor = 8;
|
let repeat_for = 8;
|
||||||
let firstChar = "*";
|
let first_char = '*';
|
||||||
|
|
||||||
if (typeof val.password !== "undefined" && val.password) {
|
if (typeof val.password !== 'undefined' && val.password) {
|
||||||
repeatFor = val.password.length - 1;
|
repeat_for = val.password.length - 1;
|
||||||
firstChar = val.password.charAt(0);
|
first_char = val.password.charAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
list.items[idx].hint = firstChar + "*".repeat(repeatFor);
|
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
|
||||||
list.items[idx].password = "";
|
list.items[idx].password = '';
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -469,7 +467,7 @@ const internalAccessList = {
|
|||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
getFilename: (list) => {
|
getFilename: (list) => {
|
||||||
return `/data/access/${list.id}`;
|
return '/data/access/' + list.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -479,64 +477,58 @@ const internalAccessList = {
|
|||||||
* @param {Array} list.items
|
* @param {Array} list.items
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
build: async (list) => {
|
build: (list) => {
|
||||||
logger.info(`Building Access file #${list.id} for: ${list.name}`);
|
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
|
||||||
|
|
||||||
const htpasswdFile = internalAccessList.getFilename(list);
|
return new Promise((resolve, reject) => {
|
||||||
|
let htpasswd_file = internalAccessList.getFilename(list);
|
||||||
|
|
||||||
// 1. remove any existing access file
|
// 1. remove any existing access file
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(htpasswdFile);
|
fs.unlinkSync(htpasswd_file);
|
||||||
} catch (_err) {
|
} catch (err) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. create empty access file
|
// 2. create empty access file
|
||||||
fs.writeFileSync(htpasswdFile, "", { encoding: "utf8" });
|
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);
|
||||||
|
|
||||||
// 3. generate password for each user
|
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
|
||||||
if (list.items.length) {
|
.then((/*result*/) => {
|
||||||
await new Promise((resolve, reject) => {
|
next();
|
||||||
batchflow(list.items)
|
})
|
||||||
.sequential()
|
.catch((err) => {
|
||||||
.each((_i, item, next) => {
|
logger.error(err);
|
||||||
if (item.password?.length) {
|
next(err);
|
||||||
// One "user:hash" line each; a ":" or line break in the name would corrupt the file
|
|
||||||
if (!isValid("accessUsername", item.username)) {
|
|
||||||
reject(unsafeValueError(`Access List #${list.id}: user name is not allowed`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info(`Adding: ${item.username}`);
|
|
||||||
|
|
||||||
utils
|
|
||||||
.execFile("openssl", ["passwd", "-apr1", item.password])
|
|
||||||
.then((res) => {
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
}
|
||||||
reject(err);
|
})
|
||||||
}
|
.error((err) => {
|
||||||
next();
|
logger.error(err);
|
||||||
})
|
reject(err);
|
||||||
.catch((err) => {
|
})
|
||||||
logger.error(err);
|
.end((results) => {
|
||||||
next(err);
|
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
|
||||||
});
|
resolve(results);
|
||||||
}
|
});
|
||||||
})
|
|
||||||
.error((err) => {
|
|
||||||
logger.error(err);
|
|
||||||
reject(err);
|
|
||||||
})
|
|
||||||
.end((results) => {
|
|
||||||
logger.success(`Built Access file #${list.id} for: ${list.name}`);
|
|
||||||
resolve(results);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalAccessList;
|
module.exports = internalAccessList;
|
||||||
@@ -1,63 +1,39 @@
|
|||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const auditLogModel = require('../models/audit-log');
|
||||||
import auditLogModel from "../models/audit-log.js";
|
|
||||||
|
|
||||||
const internalAuditLog = {
|
const internalAuditLog = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All logs
|
* All logs
|
||||||
*
|
*
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Array} [expand]
|
* @param {Array} [expand]
|
||||||
* @param {String} [searchQuery]
|
* @param {String} [search_query]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: async (access, expand, searchQuery) => {
|
getAll: (access, expand, search_query) => {
|
||||||
await access.can("auditlog:list");
|
return access.can('auditlog:list')
|
||||||
|
.then(() => {
|
||||||
|
let query = auditLogModel
|
||||||
|
.query()
|
||||||
|
.orderBy('created_on', 'DESC')
|
||||||
|
.orderBy('id', 'DESC')
|
||||||
|
.limit(100)
|
||||||
|
.allowEager('[user]');
|
||||||
|
|
||||||
const query = auditLogModel
|
// Query is used for searching
|
||||||
.query()
|
if (typeof search_query === 'string') {
|
||||||
.orderBy("created_on", "DESC")
|
query.where(function () {
|
||||||
.orderBy("id", "DESC")
|
this.where('meta', 'like', '%' + search_query + '%');
|
||||||
.limit(100)
|
});
|
||||||
.allowGraph("[user]");
|
}
|
||||||
|
|
||||||
// Query is used for searching
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
if (typeof searchQuery === "string" && searchQuery.length > 0) {
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
query.where(function () {
|
}
|
||||||
this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`);
|
|
||||||
|
return query;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {Object} [data]
|
|
||||||
* @param {Integer} [data.id] Defaults to the token user
|
|
||||||
* @param {Array} [data.expand]
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
get: async (access, data) => {
|
|
||||||
await access.can("auditlog:list");
|
|
||||||
|
|
||||||
const query = auditLogModel.query().andWhere("id", data.id).allowGraph("[user]").first();
|
|
||||||
|
|
||||||
if (typeof data.expand !== "undefined" && data.expand !== null) {
|
|
||||||
query.withGraphFetched(`[${data.expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const row = await query;
|
|
||||||
|
|
||||||
if (!row?.id) {
|
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,24 +50,29 @@ const internalAuditLog = {
|
|||||||
* @param {Object} [data.meta]
|
* @param {Object} [data.meta]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
add: async (access, data) => {
|
add: (access, data) => {
|
||||||
if (typeof data.user_id === "undefined" || !data.user_id) {
|
return new Promise((resolve, reject) => {
|
||||||
data.user_id = access.token.getUserId(1);
|
// 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) {
|
if (typeof data.action === 'undefined' || !data.action) {
|
||||||
throw new errs.InternalValidationError("Audit log entry must contain an 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
|
||||||
// Make sure at least 1 of the IDs are set and action
|
resolve(auditLogModel
|
||||||
return await auditLogModel.query().insert({
|
.query()
|
||||||
user_id: data.user_id,
|
.insert({
|
||||||
action: data.action,
|
user_id: data.user_id,
|
||||||
object_type: data.object_type || "",
|
action: data.action,
|
||||||
object_id: data.object_id || 0,
|
object_type: data.object_type || '',
|
||||||
meta: data.meta || {},
|
object_id: data.object_id || 0,
|
||||||
|
meta: data.meta || {}
|
||||||
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalAuditLog;
|
module.exports = internalAuditLog;
|
||||||
+791
-1291
File diff suppressed because it is too large.
Load diff
+358
-284
@@ -1,96 +1,102 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const deadHostModel = require('../models/dead_host');
|
||||||
import utils from "../lib/utils.js";
|
const internalHost = require('./host');
|
||||||
import deadHostModel from "../models/dead_host.js";
|
const internalNginx = require('./nginx');
|
||||||
import internalAuditLog from "./audit-log.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import internalCertificate from "./certificate.js";
|
const internalCertificate = require('./certificate');
|
||||||
import internalHost from "./host.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
const internalDeadHost = {
|
const internalDeadHost = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: async (access, data) => {
|
create: (access, data) => {
|
||||||
const createCertificate = data.certificate_id === "new";
|
let create_certificate = data.certificate_id === 'new';
|
||||||
|
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
delete data.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await access.can("dead_hosts:create", data);
|
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 = [];
|
||||||
|
|
||||||
// Get a list of the domain names and check each of them against existing records
|
data.domain_names.map(function (domain_name) {
|
||||||
const domainNameCheckPromises = [];
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
||||||
|
});
|
||||||
|
|
||||||
data.domain_names.map((domain_name) => {
|
return Promise.all(domain_name_check_promises)
|
||||||
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name));
|
.then((check_results) => {
|
||||||
return true;
|
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);
|
||||||
|
|
||||||
await Promise.all(domainNameCheckPromises).then((check_results) => {
|
return deadHostModel
|
||||||
check_results.map((result) => {
|
.query()
|
||||||
if (result.is_taken) {
|
.omit(omissions())
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
.insertAndFetch(data);
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (create_certificate) {
|
||||||
|
return internalCertificate.createQuickCertificate(access, data)
|
||||||
|
.then((cert) => {
|
||||||
|
// update host with cert id
|
||||||
|
return internalDeadHost.update(access, {
|
||||||
|
id: row.id,
|
||||||
|
certificate_id: cert.id
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return true;
|
})
|
||||||
|
.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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// At this point the domains should have been checked
|
|
||||||
data.owner_user_id = access.token.getUserId(1);
|
|
||||||
const thisData = internalHost.cleanSslHstsData(data);
|
|
||||||
|
|
||||||
// Fix for db field not having a default value
|
|
||||||
// for this optional field.
|
|
||||||
if (typeof data.advanced_config === "undefined") {
|
|
||||||
thisData.advanced_config = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
|
||||||
|
|
||||||
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
|
||||||
|
|
||||||
// Add to audit log
|
|
||||||
await internalAuditLog.add(access, {
|
|
||||||
action: "created",
|
|
||||||
object_type: "dead-host",
|
|
||||||
object_id: row.id,
|
|
||||||
meta: thisData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (createCertificate) {
|
|
||||||
const cert = await internalCertificate.createQuickCertificate(access, data);
|
|
||||||
|
|
||||||
// update host with cert id
|
|
||||||
await internalDeadHost.update(access, {
|
|
||||||
id: row.id,
|
|
||||||
certificate_id: cert.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-fetch with cert
|
|
||||||
const freshRow = await internalDeadHost.get(access, {
|
|
||||||
id: row.id,
|
|
||||||
expand: ["certificate", "owner"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
if (createCertificate && !freshRow.certificate_id) {
|
|
||||||
throw new errs.InternalValidationError("The host was created but the Certificate creation failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure nginx
|
|
||||||
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
|
|
||||||
|
|
||||||
return freshRow;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,87 +105,98 @@ const internalDeadHost = {
|
|||||||
* @param {Number} data.id
|
* @param {Number} data.id
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: async (access, data) => {
|
update: (access, data) => {
|
||||||
const createCertificate = data.certificate_id === "new";
|
let create_certificate = data.certificate_id === 'new';
|
||||||
if (createCertificate) {
|
|
||||||
|
if (create_certificate) {
|
||||||
delete data.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
await access.can("dead_hosts:update", data.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 = [];
|
||||||
|
|
||||||
// Get a list of the domain names and check each of them against existing records
|
if (typeof data.domain_names !== 'undefined') {
|
||||||
const domainNameCheckPromises = [];
|
data.domain_names.map(function (domain_name) {
|
||||||
if (typeof data.domain_names !== "undefined") {
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id));
|
||||||
data.domain_names.map((domainName) => {
|
});
|
||||||
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkResults = await Promise.all(domainNameCheckPromises);
|
return Promise.all(domain_name_check_promises)
|
||||||
checkResults.map((result) => {
|
.then((check_results) => {
|
||||||
if (result.is_taken) {
|
check_results.map(function (result) {
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
if (result.is_taken) {
|
||||||
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
})
|
||||||
|
.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());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const row = await internalDeadHost.get(access, { id: data.id });
|
|
||||||
|
|
||||||
if (row.id !== data.id) {
|
|
||||||
// Sanity check that something crazy hasn't happened
|
|
||||||
throw new errs.InternalValidationError(
|
|
||||||
`404 Host could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before the certificate is created, so a refused request doesn't leave one behind
|
|
||||||
internalHost.assertAdvancedConfigAllowed(access, data, row);
|
|
||||||
|
|
||||||
if (createCertificate) {
|
|
||||||
const cert = await internalCertificate.createQuickCertificate(access, {
|
|
||||||
domain_names: data.domain_names || row.domain_names,
|
|
||||||
meta: _.assign({}, row.meta, data.meta),
|
|
||||||
});
|
|
||||||
|
|
||||||
// update host with cert id
|
|
||||||
data.certificate_id = cert.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
|
|
||||||
let thisData = _.assign(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
domain_names: row.domain_names,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
|
||||||
|
|
||||||
// do the row update
|
|
||||||
await deadHostModel.query().where({ id: data.id }).patch(data);
|
|
||||||
|
|
||||||
// Add to audit log
|
|
||||||
await internalAuditLog.add(access, {
|
|
||||||
action: "updated",
|
|
||||||
object_type: "dead-host",
|
|
||||||
object_id: row.id,
|
|
||||||
meta: thisData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const thisRow = await internalDeadHost.get(access, {
|
|
||||||
id: thisData.id,
|
|
||||||
expand: ["owner", "certificate"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure nginx
|
|
||||||
if (!thisRow.enabled) {
|
|
||||||
// No need to add nginx config if host is disabled
|
|
||||||
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
|
|
||||||
}
|
|
||||||
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", thisRow);
|
|
||||||
thisRow.meta = newMeta;
|
|
||||||
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,32 +207,43 @@ const internalDeadHost = {
|
|||||||
* @param {Array} [data.omit]
|
* @param {Array} [data.omit]
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: async (access, data) => {
|
get: (access, data) => {
|
||||||
const accessData = await access.can("dead_hosts:get", data.id);
|
if (typeof data === 'undefined') {
|
||||||
const query = deadHostModel
|
data = {};
|
||||||
.query()
|
|
||||||
.where("is_deleted", 0)
|
|
||||||
.andWhere("id", data.id)
|
|
||||||
.allowGraph(deadHostModel.defaultAllowGraph)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (accessData.permission_visibility !== "all") {
|
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof data.expand !== "undefined" && data.expand !== null) {
|
return access.can('dead_hosts:get', data.id)
|
||||||
query.withGraphFetched(`[${data.expand.join(", ")}]`);
|
.then((access_data) => {
|
||||||
}
|
let query = deadHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.andWhere('id', data.id)
|
||||||
|
.allowEager('[owner,certificate]')
|
||||||
|
.first();
|
||||||
|
|
||||||
const row = await query.then(utils.omitRow(omissions()));
|
if (access_data.permission_visibility !== 'all') {
|
||||||
if (!row?.id) {
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
}
|
||||||
}
|
|
||||||
// Custom omissions
|
// Custom omissions
|
||||||
if (typeof data.omit !== "undefined" && data.omit !== null) {
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
return _.omit(row, data.omit);
|
query.omit(data.omit);
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (row) {
|
||||||
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
|
return _.omit(row, omissions());
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,29 +253,42 @@ const internalDeadHost = {
|
|||||||
* @param {String} [data.reason]
|
* @param {String} [data.reason]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: async (access, data) => {
|
delete: (access, data) => {
|
||||||
await access.can("dead_hosts:delete", data.id);
|
return access.can('dead_hosts:delete', data.id)
|
||||||
const row = await internalDeadHost.get(access, { id: data.id });
|
.then(() => {
|
||||||
if (!row?.id) {
|
return internalDeadHost.get(access, {id: data.id});
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
})
|
||||||
}
|
.then((row) => {
|
||||||
|
if (!row) {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
await deadHostModel.query().where("id", row.id).patch({
|
return deadHostModel
|
||||||
is_deleted: 1,
|
.query()
|
||||||
});
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
// Delete Nginx Config
|
is_deleted: 1
|
||||||
await internalNginx.deleteConfig("dead_host", row);
|
})
|
||||||
await internalNginx.reload();
|
.then(() => {
|
||||||
|
// Delete Nginx Config
|
||||||
// Add to audit log
|
return internalNginx.deleteConfig('dead_host', row)
|
||||||
await internalAuditLog.add(access, {
|
.then(() => {
|
||||||
action: "deleted",
|
return internalNginx.reload();
|
||||||
object_type: "dead-host",
|
});
|
||||||
object_id: row.id,
|
})
|
||||||
meta: _.omit(row, omissions()),
|
.then(() => {
|
||||||
});
|
// Add to audit log
|
||||||
return true;
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'deleted',
|
||||||
|
object_type: 'dead-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: _.omit(row, omissions())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,36 +298,46 @@ const internalDeadHost = {
|
|||||||
* @param {String} [data.reason]
|
* @param {String} [data.reason]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
enable: async (access, data) => {
|
enable: (access, data) => {
|
||||||
await access.can("dead_hosts:update", data.id);
|
return access.can('dead_hosts:update', data.id)
|
||||||
const row = await internalDeadHost.get(access, {
|
.then(() => {
|
||||||
id: data.id,
|
return internalDeadHost.get(access, {
|
||||||
expand: ["certificate", "owner"],
|
id: data.id,
|
||||||
});
|
expand: ['certificate', 'owner']
|
||||||
if (!row?.id) {
|
});
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
})
|
||||||
}
|
.then((row) => {
|
||||||
if (row.enabled) {
|
if (!row) {
|
||||||
throw new errs.ValidationError("Host is already enabled");
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (row.enabled) {
|
||||||
|
throw new error.ValidationError('Host is already enabled');
|
||||||
|
}
|
||||||
|
|
||||||
row.enabled = 1;
|
row.enabled = 1;
|
||||||
|
|
||||||
await deadHostModel.query().where("id", row.id).patch({
|
return deadHostModel
|
||||||
enabled: 1,
|
.query()
|
||||||
});
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
// Configure nginx
|
enabled: 1
|
||||||
await internalNginx.configure(deadHostModel, "dead_host", row);
|
})
|
||||||
|
.then(() => {
|
||||||
// Add to audit log
|
// Configure nginx
|
||||||
await internalAuditLog.add(access, {
|
return internalNginx.configure(deadHostModel, 'dead_host', row);
|
||||||
action: "enabled",
|
})
|
||||||
object_type: "dead-host",
|
.then(() => {
|
||||||
object_id: row.id,
|
// Add to audit log
|
||||||
meta: _.omit(row, omissions()),
|
return internalAuditLog.add(access, {
|
||||||
});
|
action: 'enabled',
|
||||||
return true;
|
object_type: 'dead-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: _.omit(row, omissions())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -296,34 +347,46 @@ const internalDeadHost = {
|
|||||||
* @param {String} [data.reason]
|
* @param {String} [data.reason]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
disable: async (access, data) => {
|
disable: (access, data) => {
|
||||||
await access.can("dead_hosts:update", data.id);
|
return access.can('dead_hosts:update', data.id)
|
||||||
const row = await internalDeadHost.get(access, { id: data.id });
|
.then(() => {
|
||||||
if (!row?.id) {
|
return internalDeadHost.get(access, {id: data.id});
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
})
|
||||||
}
|
.then((row) => {
|
||||||
if (!row.enabled) {
|
if (!row) {
|
||||||
throw new errs.ValidationError("Host is already disabled");
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (!row.enabled) {
|
||||||
|
throw new error.ValidationError('Host is already disabled');
|
||||||
|
}
|
||||||
|
|
||||||
row.enabled = 0;
|
row.enabled = 0;
|
||||||
|
|
||||||
await deadHostModel.query().where("id", row.id).patch({
|
return deadHostModel
|
||||||
enabled: 0,
|
.query()
|
||||||
});
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
// Delete Nginx Config
|
enabled: 0
|
||||||
await internalNginx.deleteConfig("dead_host", row);
|
})
|
||||||
await internalNginx.reload();
|
.then(() => {
|
||||||
|
// Delete Nginx Config
|
||||||
// Add to audit log
|
return internalNginx.deleteConfig('dead_host', row)
|
||||||
await internalAuditLog.add(access, {
|
.then(() => {
|
||||||
action: "disabled",
|
return internalNginx.reload();
|
||||||
object_type: "dead-host",
|
});
|
||||||
object_id: row.id,
|
})
|
||||||
meta: _.omit(row, omissions()),
|
.then(() => {
|
||||||
});
|
// Add to audit log
|
||||||
return true;
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'disabled',
|
||||||
|
object_type: 'dead-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: _.omit(row, omissions())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -331,38 +394,44 @@ const internalDeadHost = {
|
|||||||
*
|
*
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Array} [expand]
|
* @param {Array} [expand]
|
||||||
* @param {String} [searchQuery]
|
* @param {String} [search_query]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: async (access, expand, searchQuery) => {
|
getAll: (access, expand, search_query) => {
|
||||||
const accessData = await access.can("dead_hosts:list");
|
return access.can('dead_hosts:list')
|
||||||
const query = deadHostModel
|
.then((access_data) => {
|
||||||
.query()
|
let query = deadHostModel
|
||||||
.where("is_deleted", 0)
|
.query()
|
||||||
.groupBy("id")
|
.where('is_deleted', 0)
|
||||||
.allowGraph(deadHostModel.defaultAllowGraph)
|
.groupBy('id')
|
||||||
.orderBy(castJsonIfNeed("domain_names"), "ASC");
|
.omit(['is_deleted'])
|
||||||
|
.allowEager('[owner,certificate]')
|
||||||
|
.orderBy('domain_names', 'ASC');
|
||||||
|
|
||||||
if (accessData.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query is used for searching
|
// Query is used for searching
|
||||||
if (typeof searchQuery === "string" && searchQuery.length > 0) {
|
if (typeof search_query === 'string') {
|
||||||
query.where(function () {
|
query.where(function () {
|
||||||
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
|
this.where('domain_names', 'like', '%' + search_query + '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then((rows) => {
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
|
||||||
|
return internalHost.cleanAllRowsCertificateMeta(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await query.then(utils.omitRows(omissions()));
|
|
||||||
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
|
|
||||||
internalHost.cleanAllRowsCertificateMeta(rows);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -372,16 +441,21 @@ const internalDeadHost = {
|
|||||||
* @param {String} visibility
|
* @param {String} visibility
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getCount: async (user_id, visibility) => {
|
getCount: (user_id, visibility) => {
|
||||||
const query = deadHostModel.query().count("id as count").where("is_deleted", 0);
|
let query = deadHostModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0);
|
||||||
|
|
||||||
if (visibility !== "all") {
|
if (visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", user_id);
|
query.andWhere('owner_user_id', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await query.first();
|
return query.first()
|
||||||
return Number.parseInt(row.count, 10);
|
.then((row) => {
|
||||||
},
|
return parseInt(row.count, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalDeadHost;
|
module.exports = internalDeadHost;
|
||||||
+146
-220
@@ -1,109 +1,10 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const proxyHostModel = require('../models/proxy_host');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const redirectionHostModel = require('../models/redirection_host');
|
||||||
import deadHostModel from "../models/dead_host.js";
|
const deadHostModel = require('../models/dead_host');
|
||||||
import proxyHostModel from "../models/proxy_host.js";
|
const sslPassthroughHostModel = require('../models/ssl_passthrough_host');
|
||||||
import redirectionHostModel from "../models/redirection_host.js";
|
|
||||||
|
|
||||||
const internalHost = {
|
const internalHost = {
|
||||||
/**
|
|
||||||
* Only admins may set or change advanced_config (raw nginx config). Throws for anyone else.
|
|
||||||
* On create (no `row`), any non-empty advanced_config is refused, including on locations.
|
|
||||||
* On update, the host's advanced_config must be unchanged (or omitted), and locations must
|
|
||||||
* only carry advanced configs the host already has (see assertLocationAdvancedConfigUnchanged).
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {Object} data incoming host data; data.locations may be filled in on update
|
|
||||||
* @param {Object} [row] the existing host, when updating
|
|
||||||
* @throws {PermissionError}
|
|
||||||
*/
|
|
||||||
assertAdvancedConfigAllowed: (access, data, row) => {
|
|
||||||
if (access.isAdmin?.()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deny = () => {
|
|
||||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
const hasConfig = (v) => typeof v === "string" && v.trim() !== "";
|
|
||||||
if (hasConfig(data.advanced_config)) {
|
|
||||||
deny();
|
|
||||||
}
|
|
||||||
if (Array.isArray(data.locations) && data.locations.some((loc) => hasConfig(loc?.advanced_config))) {
|
|
||||||
deny();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.advanced_config !== "undefined" && data.advanced_config !== row.advanced_config) {
|
|
||||||
deny();
|
|
||||||
}
|
|
||||||
if (Array.isArray(data.locations)) {
|
|
||||||
internalHost.assertLocationAdvancedConfigUnchanged(data.locations, row.locations);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For non-admins: every location's advanced_config must be empty or one that already exists
|
|
||||||
* on this host. Matching is by value, not path, so renaming or reordering a location that
|
|
||||||
* carries admin-written config is allowed, but new or edited config is not. Each existing
|
|
||||||
* config can be used once (no duplicating it onto other paths), and same-path matches are
|
|
||||||
* taken first. Clearing a config is allowed: deleting the location is, and has the same
|
|
||||||
* effect, and a location deleted and re-added at the same path arrives with an empty config.
|
|
||||||
* A location that omits advanced_config keeps the one at its path, unless another location
|
|
||||||
* explicitly carries it. Mutates `locations`.
|
|
||||||
*
|
|
||||||
* @param {Array} locations incoming locations
|
|
||||||
* @param {Array} [existingLocations] the host's current locations
|
|
||||||
* @throws {PermissionError}
|
|
||||||
*/
|
|
||||||
assertLocationAdvancedConfigUnchanged: (locations, existingLocations) => {
|
|
||||||
const existing = Array.isArray(existingLocations) ? existingLocations : [];
|
|
||||||
const advAtPath = (path) => existing.find((l) => l.path === path)?.advanced_config || "";
|
|
||||||
const pool = existing.map((l) => l.advanced_config || "").filter(Boolean);
|
|
||||||
const take = (value) => {
|
|
||||||
const i = pool.indexOf(value);
|
|
||||||
if (i === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
pool.splice(i, 1);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
const deny = () => {
|
|
||||||
throw new errs.PermissionError("You do not have permission to modify advanced configuration");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pass 1: locations that keep the config at their own path
|
|
||||||
const unresolved = [];
|
|
||||||
const omitted = [];
|
|
||||||
for (const loc of locations) {
|
|
||||||
const atPath = advAtPath(loc.path);
|
|
||||||
if (typeof loc.advanced_config === "undefined") {
|
|
||||||
omitted.push(loc);
|
|
||||||
} else if ((loc.advanced_config || "") === atPath && (!atPath || take(atPath))) {
|
|
||||||
// unchanged
|
|
||||||
} else {
|
|
||||||
unresolved.push(loc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 2: renamed/moved locations may carry an existing config not already claimed, or
|
|
||||||
// none at all
|
|
||||||
for (const loc of unresolved) {
|
|
||||||
if (loc.advanced_config && !take(loc.advanced_config)) {
|
|
||||||
deny();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass 3: locations that omit advanced_config keep the one at their path, if no location
|
|
||||||
// that names it explicitly has claimed it. Running this last stops a new location at an
|
|
||||||
// old path taking the config away from the location it was renamed to.
|
|
||||||
for (const loc of omitted) {
|
|
||||||
const atPath = advAtPath(loc.path);
|
|
||||||
loc.advanced_config = atPath && take(atPath) ? atPath : "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sure that the ssl_* and hsts_* fields play nicely together.
|
* Makes sure that the ssl_* and hsts_* fields play nicely together.
|
||||||
@@ -114,23 +15,25 @@ const internalHost = {
|
|||||||
* @param {object} [existing_data]
|
* @param {object} [existing_data]
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
cleanSslHstsData: (data, existingData) => {
|
cleanSslHstsData: function (data, existing_data) {
|
||||||
const combinedData = _.assign({}, existingData || {}, data);
|
existing_data = existing_data === undefined ? {} : existing_data;
|
||||||
|
|
||||||
if (!combinedData.certificate_id) {
|
let combined_data = _.assign({}, existing_data, data);
|
||||||
combinedData.ssl_forced = false;
|
|
||||||
combinedData.http2_support = false;
|
if (!combined_data.certificate_id) {
|
||||||
|
combined_data.ssl_forced = false;
|
||||||
|
combined_data.http2_support = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!combinedData.ssl_forced) {
|
if (!combined_data.ssl_forced) {
|
||||||
combinedData.hsts_enabled = false;
|
combined_data.hsts_enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!combinedData.hsts_enabled) {
|
if (!combined_data.hsts_enabled) {
|
||||||
combinedData.hsts_subdomains = false;
|
combined_data.hsts_subdomains = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return combinedData;
|
return combined_data;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,12 +42,11 @@ const internalHost = {
|
|||||||
* @param {Array} rows
|
* @param {Array} rows
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
cleanAllRowsCertificateMeta: (rows) => {
|
cleanAllRowsCertificateMeta: function (rows) {
|
||||||
rows.map((_, idx) => {
|
rows.map(function (row, idx) {
|
||||||
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) {
|
if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) {
|
||||||
rows[idx].certificate.meta = {};
|
rows[idx].certificate.meta = {};
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
@@ -156,8 +58,8 @@ const internalHost = {
|
|||||||
* @param {Object} row
|
* @param {Object} row
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
cleanRowCertificateMeta: (row) => {
|
cleanRowCertificateMeta: function (row) {
|
||||||
if (typeof row.certificate !== "undefined" && row.certificate) {
|
if (typeof row.certificate !== 'undefined' && row.certificate) {
|
||||||
row.certificate.meta = {};
|
row.certificate.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,36 +67,63 @@ const internalHost = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This returns all the host types with any domain listed in the provided domainNames array.
|
* 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
|
* This is used by the certificates to temporarily disable any host that is using the domain
|
||||||
*
|
*
|
||||||
* @param {Array} domainNames
|
* @param {Array} domain_names
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getHostsWithDomains: async (domainNames) => {
|
getHostsWithDomains: function (domain_names) {
|
||||||
const responseObject = {
|
let promises = [
|
||||||
total_count: 0,
|
proxyHostModel
|
||||||
dead_hosts: [],
|
.query()
|
||||||
proxy_hosts: [],
|
.where('is_deleted', 0),
|
||||||
redirection_hosts: [],
|
redirectionHostModel
|
||||||
};
|
.query()
|
||||||
|
.where('is_deleted', 0),
|
||||||
|
deadHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0),
|
||||||
|
sslPassthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
];
|
||||||
|
|
||||||
// No relations are eager-loaded here: this scans every host just to match domains, and
|
return Promise.all(promises)
|
||||||
// the few that match are rendered through internalNginx.generateConfig(), which loads
|
.then((promises_results) => {
|
||||||
// their certificate and access list itself (loadRenderRelations).
|
let response_object = {
|
||||||
const proxyRes = await proxyHostModel.query().where("is_deleted", 0);
|
total_count: 0,
|
||||||
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
|
dead_hosts: [],
|
||||||
responseObject.total_count += responseObject.proxy_hosts.length;
|
proxy_hosts: [],
|
||||||
|
redirection_hosts: []
|
||||||
|
};
|
||||||
|
|
||||||
const redirRes = await redirectionHostModel.query().where("is_deleted", 0);
|
if (promises_results[0]) {
|
||||||
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);
|
// Proxy Hosts
|
||||||
responseObject.total_count += responseObject.redirection_hosts.length;
|
response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names);
|
||||||
|
response_object.total_count += response_object.proxy_hosts.length;
|
||||||
|
}
|
||||||
|
|
||||||
const deadRes = await deadHostModel.query().where("is_deleted", 0);
|
if (promises_results[1]) {
|
||||||
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);
|
// Redirection Hosts
|
||||||
responseObject.total_count += responseObject.dead_hosts.length;
|
response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names);
|
||||||
|
response_object.total_count += response_object.redirection_hosts.length;
|
||||||
|
}
|
||||||
|
|
||||||
return responseObject;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promises_results[3]) {
|
||||||
|
// SSL Passthrough Hosts
|
||||||
|
response_object.ssl_passthrough_hosts = internalHost._getHostsWithDomains(promises_results[3], domain_names);
|
||||||
|
response_object.total_count += response_object.ssl_passthrough_hosts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response_object;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -205,133 +134,130 @@ const internalHost = {
|
|||||||
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
|
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
isHostnameTaken: (hostname, ignore_type, ignore_id) => {
|
isHostnameTaken: function (hostname, ignore_type, ignore_id) {
|
||||||
const promises = [
|
let promises = [
|
||||||
proxyHostModel
|
proxyHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
.andWhere('domain_names', 'like', '%' + hostname + '%'),
|
||||||
redirectionHostModel
|
redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
.andWhere('domain_names', 'like', '%' + hostname + '%'),
|
||||||
deadHostModel
|
deadHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
|
.andWhere('domain_names', 'like', '%' + hostname + '%'),
|
||||||
|
sslPassthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.andWhere('domain_name', '=', hostname),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Promise.all(promises).then((promises_results) => {
|
return Promise.all(promises)
|
||||||
let is_taken = false;
|
.then((promises_results) => {
|
||||||
|
let is_taken = false;
|
||||||
|
|
||||||
if (promises_results[0]) {
|
if (promises_results[0]) {
|
||||||
// Proxy Hosts
|
// Proxy Hosts
|
||||||
if (
|
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) {
|
||||||
internalHost._checkHostnameRecordsTaken(
|
is_taken = true;
|
||||||
hostname,
|
}
|
||||||
promises_results[0],
|
|
||||||
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
is_taken = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (promises_results[1]) {
|
if (promises_results[1]) {
|
||||||
// Redirection Hosts
|
// Redirection Hosts
|
||||||
if (
|
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) {
|
||||||
internalHost._checkHostnameRecordsTaken(
|
is_taken = true;
|
||||||
hostname,
|
}
|
||||||
promises_results[1],
|
|
||||||
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
is_taken = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (promises_results[2]) {
|
if (promises_results[2]) {
|
||||||
// Dead Hosts
|
// Dead Hosts
|
||||||
if (
|
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
|
||||||
internalHost._checkHostnameRecordsTaken(
|
is_taken = true;
|
||||||
hostname,
|
}
|
||||||
promises_results[2],
|
|
||||||
ignore_type === "dead" && ignore_id ? ignore_id : 0,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
is_taken = true;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (promises_results[3]) {
|
||||||
hostname: hostname,
|
// SSL Passthrough Hosts
|
||||||
is_taken: is_taken,
|
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[3], ignore_type === 'ssl_passthrough' && ignore_id ? ignore_id : 0)) {
|
||||||
};
|
is_taken = true;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname: hostname,
|
||||||
|
is_taken: is_taken
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private call only
|
* Private call only
|
||||||
*
|
*
|
||||||
* @param {String} hostname
|
* @param {String} hostname
|
||||||
* @param {Array} existingRows
|
* @param {Array} existing_rows
|
||||||
* @param {Integer} [ignoreId]
|
* @param {Integer} [ignore_id]
|
||||||
* @returns {Boolean}
|
* @returns {Boolean}
|
||||||
*/
|
*/
|
||||||
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {
|
_checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) {
|
||||||
let isTaken = false;
|
let is_taken = false;
|
||||||
|
|
||||||
if (existingRows?.length) {
|
if (existing_rows && existing_rows.length) {
|
||||||
existingRows.map((existingRow) => {
|
existing_rows.map(function (existing_row) {
|
||||||
existingRow.domain_names.map((existingHostname) => {
|
|
||||||
|
function checkHostname(existing_hostname) {
|
||||||
// Does this domain match?
|
// Does this domain match?
|
||||||
if (existingHostname.toLowerCase() === hostname.toLowerCase()) {
|
if (existing_hostname.toLowerCase() === hostname.toLowerCase()) {
|
||||||
if (!ignoreId || ignoreId !== existingRow.id) {
|
if (!ignore_id || ignore_id !== existing_row.id) {
|
||||||
isTaken = true;
|
is_taken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
}
|
||||||
});
|
|
||||||
return true;
|
if (existing_row.domain_names) {
|
||||||
|
existing_row.domain_names.map(checkHostname);
|
||||||
|
} else if (existing_row.domain_name) {
|
||||||
|
checkHostname(existing_row.domain_name);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return isTaken;
|
return is_taken;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private call only
|
* Private call only
|
||||||
*
|
*
|
||||||
* @param {Array} hosts
|
* @param {Array} hosts
|
||||||
* @param {Array} domainNames
|
* @param {Array} domain_names
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
_getHostsWithDomains: (hosts, domainNames) => {
|
_getHostsWithDomains: function (hosts, domain_names) {
|
||||||
const response = [];
|
let response = [];
|
||||||
|
|
||||||
if (hosts?.length) {
|
if (hosts && hosts.length) {
|
||||||
hosts.map((host) => {
|
hosts.map(function (host) {
|
||||||
let hostMatches = false;
|
let host_matches = false;
|
||||||
|
|
||||||
domainNames.map((domainName) => {
|
domain_names.map(function (domain_name) {
|
||||||
host.domain_names.map((hostDomainName) => {
|
host.domain_names.map(function (host_domain_name) {
|
||||||
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) {
|
if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) {
|
||||||
hostMatches = true;
|
host_matches = true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hostMatches) {
|
if (host_matches) {
|
||||||
response.push(host);
|
response.push(host);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalHost;
|
module.exports = internalHost;
|
||||||
@@ -1,53 +1,42 @@
|
|||||||
import fs from "node:fs";
|
const https = require('https');
|
||||||
import https from "node:https";
|
const fs = require('fs');
|
||||||
import { dirname } from "node:path";
|
const logger = require('../logger').ip_ranges;
|
||||||
import { fileURLToPath } from "node:url";
|
const error = require('../lib/error');
|
||||||
import { ProxyAgent } from "proxy-agent";
|
const internalNginx = require('./nginx');
|
||||||
import errs from "../lib/error.js";
|
const { Liquid } = require('liquidjs');
|
||||||
import utils from "../lib/utils.js";
|
|
||||||
import { ipRanges as logger } from "../logger.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json';
|
||||||
const __dirname = dirname(__filename);
|
const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4';
|
||||||
|
const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6';
|
||||||
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 = {
|
const internalIpRanges = {
|
||||||
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
|
|
||||||
interval: null,
|
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
|
||||||
|
interval: null,
|
||||||
interval_processing: false,
|
interval_processing: false,
|
||||||
iteration_count: 0,
|
iteration_count: 0,
|
||||||
|
|
||||||
initTimer: () => {
|
initTimer: () => {
|
||||||
logger.info("IP Ranges Renewal Timer initialized");
|
logger.info('IP Ranges Renewal Timer initialized');
|
||||||
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
|
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchUrl: (url) => {
|
fetchUrl: (url) => {
|
||||||
const agent = new ProxyAgent();
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
logger.info(`Fetching ${url}`);
|
logger.info('Fetching ' + url);
|
||||||
return https
|
return https.get(url, (res) => {
|
||||||
.get(url, { agent }, (res) => {
|
res.setEncoding('utf8');
|
||||||
res.setEncoding("utf8");
|
let raw_data = '';
|
||||||
let raw_data = "";
|
res.on('data', (chunk) => {
|
||||||
res.on("data", (chunk) => {
|
raw_data += chunk;
|
||||||
raw_data += chunk;
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("end", () => {
|
|
||||||
resolve(raw_data);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.on("error", (err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
resolve(raw_data);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -57,30 +46,27 @@ const internalIpRanges = {
|
|||||||
fetch: () => {
|
fetch: () => {
|
||||||
if (!internalIpRanges.interval_processing) {
|
if (!internalIpRanges.interval_processing) {
|
||||||
internalIpRanges.interval_processing = true;
|
internalIpRanges.interval_processing = true;
|
||||||
logger.info("Fetching IP Ranges from online services...");
|
logger.info('Fetching IP Ranges from online services...');
|
||||||
|
|
||||||
let ip_ranges = [];
|
let ip_ranges = [];
|
||||||
|
|
||||||
return internalIpRanges
|
return internalIpRanges.fetchUrl(CLOUDFRONT_URL)
|
||||||
.fetchUrl(CLOUDFRONT_URL)
|
|
||||||
.then((cloudfront_data) => {
|
.then((cloudfront_data) => {
|
||||||
const data = JSON.parse(cloudfront_data);
|
let data = JSON.parse(cloudfront_data);
|
||||||
|
|
||||||
if (data && typeof data.prefixes !== "undefined") {
|
if (data && typeof data.prefixes !== 'undefined') {
|
||||||
data.prefixes.map((item) => {
|
data.prefixes.map((item) => {
|
||||||
if (item.service === "CLOUDFRONT") {
|
if (item.service === 'CLOUDFRONT') {
|
||||||
ip_ranges.push(item.ip_prefix);
|
ip_ranges.push(item.ip_prefix);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && typeof data.ipv6_prefixes !== "undefined") {
|
if (data && typeof data.ipv6_prefixes !== 'undefined') {
|
||||||
data.ipv6_prefixes.map((item) => {
|
data.ipv6_prefixes.map((item) => {
|
||||||
if (item.service === "CLOUDFRONT") {
|
if (item.service === 'CLOUDFRONT') {
|
||||||
ip_ranges.push(item.ipv6_prefix);
|
ip_ranges.push(item.ipv6_prefix);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -88,38 +74,38 @@ const internalIpRanges = {
|
|||||||
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
|
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
|
||||||
})
|
})
|
||||||
.then((cloudfare_data) => {
|
.then((cloudfare_data) => {
|
||||||
const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line));
|
let items = cloudfare_data.split('\n');
|
||||||
ip_ranges = [...ip_ranges, ...items];
|
ip_ranges = [... ip_ranges, ... items];
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
|
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
|
||||||
})
|
})
|
||||||
.then((cloudfare_data) => {
|
.then((cloudfare_data) => {
|
||||||
const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line));
|
let items = cloudfare_data.split('\n');
|
||||||
ip_ranges = [...ip_ranges, ...items];
|
ip_ranges = [... ip_ranges, ... items];
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const clean_ip_ranges = [];
|
let clean_ip_ranges = [];
|
||||||
ip_ranges.map((range) => {
|
ip_ranges.map((range) => {
|
||||||
if (range) {
|
if (range) {
|
||||||
clean_ip_ranges.push(range);
|
clean_ip_ranges.push(range);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return internalIpRanges.generateConfig(clean_ip_ranges).then(() => {
|
return internalIpRanges.generateConfig(clean_ip_ranges)
|
||||||
if (internalIpRanges.iteration_count) {
|
.then(() => {
|
||||||
// Reload nginx
|
if (internalIpRanges.iteration_count) {
|
||||||
return internalNginx.reload();
|
// Reload nginx
|
||||||
}
|
return internalNginx.reload();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
internalIpRanges.interval_processing = false;
|
internalIpRanges.interval_processing = false;
|
||||||
internalIpRanges.iteration_count++;
|
internalIpRanges.iteration_count++;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.fatal(err.message);
|
logger.error(err.message);
|
||||||
internalIpRanges.interval_processing = false;
|
internalIpRanges.interval_processing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -130,29 +116,32 @@ const internalIpRanges = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
generateConfig: (ip_ranges) => {
|
generateConfig: (ip_ranges) => {
|
||||||
const renderEngine = utils.getRenderEngine();
|
let renderEngine = new Liquid({
|
||||||
|
root: __dirname + '/../templates/'
|
||||||
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let template = null;
|
let template = null;
|
||||||
const filename = "/etc/nginx/conf.d/include/ip_ranges.conf";
|
let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';
|
||||||
try {
|
try {
|
||||||
template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" });
|
template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
reject(new error.ConfigurationError(err.message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEngine
|
renderEngine
|
||||||
.parseAndRender(template, { ip_ranges: ip_ranges })
|
.parseAndRender(template, {ip_ranges: ip_ranges})
|
||||||
.then((config_text) => {
|
.then((config_text) => {
|
||||||
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
|
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
||||||
resolve(true);
|
resolve(true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.warn(`Could not write ${filename}: ${err.message}`);
|
logger.warn('Could not write ' + filename + ':', err.message);
|
||||||
reject(new errs.ConfigurationError(err.message));
|
reject(new error.ConfigurationError(err.message));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalIpRanges;
|
module.exports = internalIpRanges;
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import errs from "../lib/error.js";
|
|
||||||
import internalDeadHost from "./dead-host.js";
|
|
||||||
import internalProxyHost from "./proxy-host.js";
|
|
||||||
import internalRedirectionHost from "./redirection-host.js";
|
|
||||||
import internalStream from "./stream.js";
|
|
||||||
|
|
||||||
const SYSTEM_LOG_FILE = "/data/logs/backend.log";
|
|
||||||
const LETSENCRYPT_LOG_FILE = "/data/logs/letsencrypt.log";
|
|
||||||
|
|
||||||
// Matches the access_log/error_log paths written by the nginx templates
|
|
||||||
// (see backend/templates/{proxy_host,redirection_host,dead_host,stream}.conf).
|
|
||||||
// This is a fixed, server-side lookup table - a host log path is always derived
|
|
||||||
// from a validated `host_type` enum + numeric `host_id`, never from a client-supplied
|
|
||||||
// path or filename, so there is no path-traversal surface here.
|
|
||||||
const HOST_FILE_PREFIX = {
|
|
||||||
proxy: "proxy-host",
|
|
||||||
redirection: "redirection-host",
|
|
||||||
dead: "dead-host",
|
|
||||||
stream: "stream",
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_LINES = 200;
|
|
||||||
const MAX_LINES = 1000;
|
|
||||||
const CHUNK_SIZE = 64 * 1024;
|
|
||||||
// Never scan further back than this, regardless of how many lines were requested,
|
|
||||||
// so a huge or pathological log file can't turn a single request into unbounded I/O.
|
|
||||||
const MAX_SCAN_BYTES = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads at most `maxLines` lines from the end of a file, without loading the
|
|
||||||
* whole file into memory. Reads backwards in fixed-size chunks until enough
|
|
||||||
* newlines have been seen, the start of the file is reached, or the hard
|
|
||||||
* MAX_SCAN_BYTES ceiling is hit.
|
|
||||||
*
|
|
||||||
* @param {String} filePath
|
|
||||||
* @param {Number} maxLines
|
|
||||||
* @returns {Promise<{lines: String[], size: Number, truncated: Boolean}>}
|
|
||||||
*/
|
|
||||||
const readLastLines = async (filePath, maxLines) => {
|
|
||||||
let handle;
|
|
||||||
try {
|
|
||||||
handle = await fs.promises.open(filePath, "r");
|
|
||||||
const stat = await handle.stat();
|
|
||||||
const { size } = stat;
|
|
||||||
|
|
||||||
if (size === 0) {
|
|
||||||
return { lines: [], size: 0, truncated: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
let position = size;
|
|
||||||
let scanned = 0;
|
|
||||||
let newlineCount = 0;
|
|
||||||
const chunks = [];
|
|
||||||
|
|
||||||
while (position > 0 && newlineCount <= maxLines && scanned < MAX_SCAN_BYTES) {
|
|
||||||
const readSize = Math.min(CHUNK_SIZE, position);
|
|
||||||
position -= readSize;
|
|
||||||
const buffer = Buffer.alloc(readSize);
|
|
||||||
await handle.read(buffer, 0, readSize, position);
|
|
||||||
scanned += readSize;
|
|
||||||
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
||||||
if (buffer[i] === 0x0a) newlineCount++;
|
|
||||||
}
|
|
||||||
chunks.unshift(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const truncated = position > 0 && scanned >= MAX_SCAN_BYTES;
|
|
||||||
|
|
||||||
// If we didn't start reading from byte 0, the first line in our buffer is only
|
|
||||||
// partial *unless* it happens that `position` landed exactly on a line boundary
|
|
||||||
// (the byte right before it is a newline) - check that one byte to avoid
|
|
||||||
// silently dropping a perfectly valid line.
|
|
||||||
let startsOnLineBoundary = position === 0;
|
|
||||||
if (position > 0) {
|
|
||||||
const boundaryByte = Buffer.alloc(1);
|
|
||||||
await handle.read(boundaryByte, 0, 1, position - 1);
|
|
||||||
startsOnLineBoundary = boundaryByte[0] === 0x0a;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = Buffer.concat(chunks).toString("utf8");
|
|
||||||
const allLines = text.split("\n");
|
|
||||||
|
|
||||||
if (!startsOnLineBoundary && allLines.length > 0) {
|
|
||||||
allLines.shift();
|
|
||||||
}
|
|
||||||
// Drop the trailing empty element caused by a final trailing newline.
|
|
||||||
if (allLines.length > 0 && allLines[allLines.length - 1] === "") {
|
|
||||||
allLines.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { lines: allLines.slice(-maxLines), size, truncated };
|
|
||||||
} finally {
|
|
||||||
if (handle) {
|
|
||||||
await handle.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a validated {type, host_type, host_id, channel} selection to the
|
|
||||||
* fixed, absolute path of the log file on disk. Throws if the combination
|
|
||||||
* isn't a recognised source.
|
|
||||||
*
|
|
||||||
* `channel` ("access" | "error") picks which of the two log files nginx writes
|
|
||||||
* per host - it's deliberately not named "stream" to avoid confusion with the
|
|
||||||
* "stream" host_type (TCP/UDP stream hosts).
|
|
||||||
*
|
|
||||||
* @param {Object} data
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
const resolveFilePath = (data) => {
|
|
||||||
switch (data.type) {
|
|
||||||
case "system":
|
|
||||||
return SYSTEM_LOG_FILE;
|
|
||||||
case "letsencrypt":
|
|
||||||
return LETSENCRYPT_LOG_FILE;
|
|
||||||
case "host": {
|
|
||||||
const prefix = HOST_FILE_PREFIX[data.host_type];
|
|
||||||
if (!prefix || !data.host_id || !["access", "error"].includes(data.channel)) {
|
|
||||||
throw new errs.ValidationError("Invalid host log source");
|
|
||||||
}
|
|
||||||
return `/data/logs/${prefix}-${data.host_id}_${data.channel}.log`;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new errs.ItemNotFoundError(data.type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array} rows
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
const toHostOptions = (rows) =>
|
|
||||||
rows.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
label: Array.isArray(row.domain_names) ? row.domain_names.join(", ") : `Host #${row.id}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const internalLogViewer = {
|
|
||||||
/**
|
|
||||||
* Lists the log sources available for the log viewer: the system (backend)
|
|
||||||
* log, the Let's Encrypt (certbot) log, and one entry per host the caller
|
|
||||||
* can see, for each host type.
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
listSources: async (access) => {
|
|
||||||
await access.can("logs:list");
|
|
||||||
|
|
||||||
const [proxyHosts, redirectionHosts, deadHosts, streams] = await Promise.all([
|
|
||||||
internalProxyHost.getAll(access),
|
|
||||||
internalRedirectionHost.getAll(access),
|
|
||||||
internalDeadHost.getAll(access),
|
|
||||||
internalStream.getAll(access),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
system: { label: "System" },
|
|
||||||
letsencrypt: { label: "Let's Encrypt" },
|
|
||||||
hosts: {
|
|
||||||
proxy: toHostOptions(proxyHosts),
|
|
||||||
redirection: toHostOptions(redirectionHosts),
|
|
||||||
dead: toHostOptions(deadHosts),
|
|
||||||
stream: streams.map((row) => ({
|
|
||||||
id: row.id,
|
|
||||||
label: `Port ${row.incoming_port} → ${row.forwarding_host}:${row.forwarding_port}`,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the last N lines of the requested log source, optionally
|
|
||||||
* filtered by level and/or a plain-text search term.
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
|
||||||
* @param {Object} data
|
|
||||||
* @param {String} data.type "system" | "letsencrypt" | "host"
|
|
||||||
* @param {String} [data.host_type] "proxy" | "redirection" | "dead" | "stream"
|
|
||||||
* @param {Number} [data.host_id]
|
|
||||||
* @param {String} [data.channel] "access" | "error"
|
|
||||||
* @param {Number} [data.lines]
|
|
||||||
* @param {String} [data.level]
|
|
||||||
* @param {String} [data.search]
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
tail: async (access, data) => {
|
|
||||||
await access.can("logs:list");
|
|
||||||
|
|
||||||
const filePath = resolveFilePath(data);
|
|
||||||
const lines = Math.min(Math.max(data.lines || DEFAULT_LINES, 1), MAX_LINES);
|
|
||||||
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = await readLastLines(filePath, lines);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code === "ENOENT") {
|
|
||||||
return { lines: [], size: 0, truncated: false, exists: false };
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
let outputLines = result.lines;
|
|
||||||
|
|
||||||
// Only the system log is written in our own "LEVEL [scope]" format - level
|
|
||||||
// filtering on nginx/certbot lines would just match nothing and look like an
|
|
||||||
// empty log, so the filter is a no-op for any other source.
|
|
||||||
if (data.type === "system" && data.level) {
|
|
||||||
const needle = ` ${data.level.toUpperCase().padEnd(7)} `;
|
|
||||||
outputLines = outputLines.filter((line) => line.includes(needle));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.search) {
|
|
||||||
const needle = data.search.toLowerCase();
|
|
||||||
outputLines = outputLines.filter((line) => line.toLowerCase().includes(needle));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
lines: outputLines,
|
|
||||||
size: result.size,
|
|
||||||
truncated: result.truncated,
|
|
||||||
exists: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default internalLogViewer;
|
|
||||||
+303
-353
@@ -1,19 +1,14 @@
|
|||||||
import fs from "node:fs";
|
const _ = require('lodash');
|
||||||
import net from "node:net";
|
const fs = require('fs');
|
||||||
import { dirname } from "node:path";
|
const logger = require('../logger').nginx;
|
||||||
import { fileURLToPath } from "node:url";
|
const utils = require('../lib/utils');
|
||||||
import _ from "lodash";
|
const error = require('../lib/error');
|
||||||
import errs from "../lib/error.js";
|
const { Liquid } = require('liquidjs');
|
||||||
import { assertAccessListRenderable, assertHostRenderable, isTokenSafe, unsafeValueError } from "../lib/nginx-values.js";
|
const passthroughHostModel = require('../models/ssl_passthrough_host');
|
||||||
import utils from "../lib/utils.js";
|
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
|
||||||
import { debug, nginx as logger } from "../logger.js";
|
|
||||||
import accessListModel from "../models/access_list.js";
|
|
||||||
import certificateModel from "../models/certificate.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const internalNginx = {
|
const internalNginx = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will:
|
* This will:
|
||||||
* - test the nginx config first to make sure it's OK
|
* - test the nginx config first to make sure it's OK
|
||||||
@@ -29,91 +24,111 @@ const internalNginx = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
configure: (model, host_type, host) => {
|
configure: (model, host_type, host) => {
|
||||||
let combined_meta = {};
|
let combined_meta = {};
|
||||||
|
const sslPassthroughEnabled = internalNginx.sslPassthroughEnabled();
|
||||||
|
|
||||||
return internalNginx
|
return internalNginx.test()
|
||||||
.test()
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Nginx is OK
|
// Nginx is OK
|
||||||
// We're deleting this config regardless.
|
// We're deleting this config regardless.
|
||||||
// Don't throw errors, as the file may not exist at all
|
return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all
|
||||||
// Delete the .err file too
|
|
||||||
return internalNginx.deleteConfig(host_type, host, true);
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalNginx.generateConfig(host_type, host).then(
|
if (host_type === 'ssl_passthrough_host' && !sslPassthroughEnabled){
|
||||||
() => true,
|
// ssl passthrough is disabled
|
||||||
(err) => {
|
const meta = {
|
||||||
if (!err.unsafeNginxValue) {
|
nginx_online: false,
|
||||||
throw err;
|
nginx_err: 'SSL passthrough is not enabled in environment'
|
||||||
}
|
};
|
||||||
// Nothing was written. Report it on the host like a failed nginx test.
|
|
||||||
logger.error(err.message);
|
return passthroughHostModel
|
||||||
combined_meta = _.assign({}, host.meta, {
|
.query()
|
||||||
nginx_online: false,
|
.where('is_deleted', 0)
|
||||||
nginx_err: err.message,
|
.andWhere('enabled', 1)
|
||||||
|
.patch({
|
||||||
|
meta
|
||||||
|
}).then(() => {
|
||||||
|
return internalNginx.deleteConfig('ssl_passthrough_host', host, false);
|
||||||
});
|
});
|
||||||
return model
|
} else {
|
||||||
.query()
|
return internalNginx.generateConfig(host_type, host);
|
||||||
.where("id", host.id)
|
|
||||||
.patch({ meta: combined_meta })
|
|
||||||
.then(() => false);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then((generated) => {
|
|
||||||
if (!generated) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
// Test nginx again and update meta with result
|
// Test nginx again and update meta with result
|
||||||
return internalNginx
|
return internalNginx.test()
|
||||||
.test()
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// nginx is ok
|
// nginx is ok
|
||||||
combined_meta = _.assign({}, host.meta, {
|
combined_meta = _.assign({}, host.meta, {
|
||||||
nginx_online: true,
|
nginx_online: true,
|
||||||
nginx_err: null,
|
nginx_err: null
|
||||||
});
|
});
|
||||||
|
|
||||||
return model.query().where("id", host.id).patch({
|
if (host_type === 'ssl_passthrough_host'){
|
||||||
meta: combined_meta,
|
// If passthrough is disabled we have already marked the hosts as offline
|
||||||
});
|
if (sslPassthroughEnabled) {
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.andWhere('enabled', 1)
|
||||||
|
.patch({
|
||||||
|
meta: combined_meta
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return model
|
||||||
|
.query()
|
||||||
|
.where('id', host.id)
|
||||||
|
.patch({
|
||||||
|
meta: combined_meta
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
|
// 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:
|
// 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)
|
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
|
||||||
|
|
||||||
const valid_lines = [];
|
let valid_lines = [];
|
||||||
const err_lines = err.message.split("\n");
|
let err_lines = err.message.split('\n');
|
||||||
err_lines.map((line) => {
|
err_lines.map(function (line) {
|
||||||
if (line.indexOf("/var/log/nginx/error.log") === -1) {
|
if (line.indexOf('/var/log/nginx/error.log') === -1) {
|
||||||
valid_lines.push(line);
|
valid_lines.push(line);
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
debug(logger, "Nginx test failed:", valid_lines.join("\n"));
|
if (debug_mode) {
|
||||||
|
logger.error('Nginx test failed:', valid_lines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
// config is bad, update meta and delete config
|
// config is bad, update meta and delete config
|
||||||
combined_meta = _.assign({}, host.meta, {
|
combined_meta = _.assign({}, host.meta, {
|
||||||
nginx_online: false,
|
nginx_online: false,
|
||||||
nginx_err: valid_lines.join("\n"),
|
nginx_err: valid_lines.join('\n')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (host_type === 'ssl_passthrough_host'){
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.andWhere('enabled', 1)
|
||||||
|
.patch({
|
||||||
|
meta: combined_meta
|
||||||
|
}).then(() => {
|
||||||
|
return internalNginx.deleteConfig('ssl_passthrough_host', host, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return model
|
return model
|
||||||
.query()
|
.query()
|
||||||
.where("id", host.id)
|
.where('id', host.id)
|
||||||
.patch({
|
.patch({
|
||||||
meta: combined_meta,
|
meta: combined_meta
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Keep the failed config as a .err file for inspection
|
return internalNginx.deleteConfig(host_type, host, true);
|
||||||
return internalNginx.renameConfigAsError(host_type, host);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
// The rename removed the live config already, don't touch the .err file
|
|
||||||
return internalNginx.deleteConfig(host_type, host, false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -129,18 +144,22 @@ const internalNginx = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
test: () => {
|
test: () => {
|
||||||
debug(logger, "Testing Nginx configuration");
|
if (debug_mode) {
|
||||||
return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]);
|
logger.info('Testing Nginx configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
reload: () => {
|
reload: () => {
|
||||||
return internalNginx.test().then(() => {
|
return internalNginx.test()
|
||||||
logger.info("Reloading Nginx");
|
.then(() => {
|
||||||
return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]);
|
logger.info('Reloading Nginx');
|
||||||
});
|
return utils.exec('/usr/sbin/nginx -s reload');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,10 +168,15 @@ const internalNginx = {
|
|||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
getConfigName: (host_type, host_id) => {
|
getConfigName: (host_type, host_id) => {
|
||||||
if (host_type === "default") {
|
host_type = host_type.replace(new RegExp('-', 'g'), '_');
|
||||||
return "/data/nginx/default_host/site.conf";
|
|
||||||
|
if (host_type === 'default') {
|
||||||
|
return '/data/nginx/default_host/site.conf';
|
||||||
|
} else if (host_type === 'ssl_passthrough_host') {
|
||||||
|
return '/data/nginx/ssl_passthrough_host/hosts.conf';
|
||||||
}
|
}
|
||||||
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`;
|
|
||||||
|
return '/data/nginx/' + host_type + '/' + host_id + '.conf';
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,83 +185,48 @@ const internalNginx = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
renderLocations: (host) => {
|
renderLocations: (host) => {
|
||||||
|
|
||||||
|
//logger.info('host = ' + JSON.stringify(host, null, 2));
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let template;
|
let template;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" });
|
template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
reject(new error.ConfigurationError(err.message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderEngine = utils.getRenderEngine();
|
let renderer = new Liquid({
|
||||||
let renderedLocations = "";
|
root: __dirname + '/../templates/'
|
||||||
|
});
|
||||||
|
let renderedLocations = '';
|
||||||
|
|
||||||
const locationRendering = async () => {
|
const locationRendering = async () => {
|
||||||
for (let i = 0; i < host.locations.length; i++) {
|
for (let i = 0; i < host.locations.length; i++) {
|
||||||
const locationCopy = Object.assign(
|
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},
|
||||||
{ access_list_id: host.access_list_id },
|
{allow_websocket_upgrade: host.allow_websocket_upgrade}, {http2_support: host.http2_support},
|
||||||
{ certificate_id: host.certificate_id },
|
{hsts_enabled: host.hsts_enabled}, {hsts_subdomains: host.hsts_subdomains}, {access_list: host.access_list},
|
||||||
{ ssl_forced: host.ssl_forced },
|
{certificate: host.certificate}, host.locations[i]);
|
||||||
{ caching_enabled: host.caching_enabled },
|
|
||||||
{ block_exploits: host.block_exploits },
|
if (locationCopy.forward_host.indexOf('/') > -1) {
|
||||||
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
|
const splitted = locationCopy.forward_host.split('/');
|
||||||
{ 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],
|
|
||||||
);
|
|
||||||
|
|
||||||
// A location with its own access list overrides the host's,
|
|
||||||
// otherwise it inherits the host's access list
|
|
||||||
let locationAccessList = null;
|
|
||||||
if (locationCopy.access_list_id > 0 && locationCopy.access_list_id !== host.access_list?.id) {
|
|
||||||
locationAccessList = await accessListModel
|
|
||||||
.query()
|
|
||||||
.where("is_deleted", 0)
|
|
||||||
.andWhere("id", locationCopy.access_list_id)
|
|
||||||
.withGraphFetched("[clients,items]")
|
|
||||||
.first();
|
|
||||||
}
|
|
||||||
if (locationAccessList) {
|
|
||||||
assertAccessListRenderable(
|
|
||||||
`Proxy Host #${host.id} location ${locationCopy.path}`,
|
|
||||||
locationAccessList,
|
|
||||||
);
|
|
||||||
locationCopy.access_list = locationAccessList;
|
|
||||||
} else if (
|
|
||||||
locationCopy.access_list_id > 0 &&
|
|
||||||
locationCopy.access_list_id !== host.access_list?.id
|
|
||||||
) {
|
|
||||||
// The location names its own list but it's gone. Don't fall back to the
|
|
||||||
// host's (possibly less restrictive) list; _access.conf denies all instead.
|
|
||||||
logger.error(
|
|
||||||
`Proxy Host #${host.id} location "${locationCopy.path}" references Access List #${locationCopy.access_list_id} which does not exist, all access will be denied`,
|
|
||||||
);
|
|
||||||
locationCopy.access_list = null;
|
|
||||||
} else {
|
|
||||||
locationCopy.access_list_id = host.access_list_id;
|
|
||||||
locationCopy.access_list = host.access_list;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locationCopy.forward_host.indexOf("/") > -1) {
|
|
||||||
const splitted = locationCopy.forward_host.split("/");
|
|
||||||
|
|
||||||
locationCopy.forward_host = splitted.shift();
|
locationCopy.forward_host = splitted.shift();
|
||||||
locationCopy.forward_path = `/${splitted.join("/")}`;
|
locationCopy.forward_path = `/${splitted.join('/')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
|
//logger.info('locationCopy = ' + JSON.stringify(locationCopy, null, 2));
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
renderedLocations += await renderer.parseAndRender(template, locationCopy);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
locationRendering()
|
locationRendering().then(() => resolve(renderedLocations));
|
||||||
.then(() => resolve(renderedLocations))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -246,151 +235,103 @@ const internalNginx = {
|
|||||||
* @param {Object} host
|
* @param {Object} host
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
generateConfig: async (host_type, host_row) => {
|
generateConfig: async (host_type, host) => {
|
||||||
// Prevent modifying the original object:
|
host_type = host_type.replace(new RegExp('-', 'g'), '_');
|
||||||
const host = JSON.parse(JSON.stringify(host_row));
|
|
||||||
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
|
|
||||||
|
|
||||||
await internalNginx.loadRenderRelations(nice_host_type, host);
|
if (debug_mode) {
|
||||||
|
logger.info('Generating ' + host_type + ' Config:', host);
|
||||||
|
}
|
||||||
|
|
||||||
// Values are written into the config as-is; refuse any that could add directives
|
// logger.info('host = ' + JSON.stringify(host, null, 2));
|
||||||
assertHostRenderable(nice_host_type, host);
|
|
||||||
|
|
||||||
debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2));
|
let renderEngine = new Liquid({
|
||||||
|
root: __dirname + '/../templates/'
|
||||||
|
});
|
||||||
|
|
||||||
const renderEngine = utils.getRenderEngine();
|
let template = null;
|
||||||
|
let filename = internalNginx.getConfigName(host_type, host.id);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
let template = null;
|
template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
|
||||||
const filename = internalNginx.getConfigName(nice_host_type, host.id);
|
} catch (err) {
|
||||||
|
throw new error.ConfigurationError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
let locationsPromise;
|
||||||
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" });
|
let origLocations;
|
||||||
} catch (err) {
|
|
||||||
reject(new errs.ConfigurationError(err.message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let locationsPromise;
|
// Manipulate the data a bit before sending it to the template
|
||||||
let origLocations;
|
if (host_type === 'ssl_passthrough_host') {
|
||||||
|
if (internalNginx.sslPassthroughEnabled()){
|
||||||
// Manipulate the data a bit before sending it to the template
|
const allHosts = await passthroughHostModel
|
||||||
if (nice_host_type !== "default") {
|
.query()
|
||||||
host.use_default_location = true;
|
.where('is_deleted', 0)
|
||||||
if (typeof host.advanced_config !== "undefined" && host.advanced_config) {
|
.groupBy('id')
|
||||||
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
|
.omit(['is_deleted']);
|
||||||
}
|
host = {
|
||||||
}
|
all_passthrough_hosts: allHosts.map((host) => {
|
||||||
|
// Replace dots in domain
|
||||||
// For redirection hosts, if the scheme is not http or https, set it to $scheme
|
host.forwarding_host = internalNginx.addIpv6Brackets(host.forwarding_host);
|
||||||
if (
|
return host;
|
||||||
nice_host_type === "redirection_host" &&
|
}),
|
||||||
["http", "https"].indexOf(host.forward_scheme.toLowerCase()) === -1
|
};
|
||||||
) {
|
|
||||||
host.forward_scheme = "$scheme";
|
|
||||||
}
|
|
||||||
|
|
||||||
// A stream forwarding to an IPv6 literal must have the address wrapped in
|
|
||||||
// square brackets before nginx appends ":<port>". Without the brackets nginx
|
|
||||||
// reads the trailing ":<port>" as part of the address and rejects the upstream
|
|
||||||
// ("invalid port in upstream"), so the stream saves but never activates (#5740).
|
|
||||||
if (nice_host_type === "stream" && net.isIPv6(host.forwarding_host)) {
|
|
||||||
host.forwarding_host = `[${host.forwarding_host}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
locationsPromise = Promise.resolve();
|
internalNginx.deleteConfig(host_type, host, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if (host_type !== 'default') {
|
||||||
|
host.use_default_location = true;
|
||||||
|
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
|
||||||
|
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set the IPv6 setting for the host
|
if (host.locations) {
|
||||||
host.ipv6 = internalNginx.ipv6Enabled();
|
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
|
||||||
|
origLocations = [].concat(host.locations);
|
||||||
|
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
|
||||||
|
host.locations = renderedLocations;
|
||||||
|
});
|
||||||
|
|
||||||
locationsPromise
|
// Allow someone who is using / custom location path to use it, and skip the default / location
|
||||||
.then(() => {
|
_.map(host.locations, (location) => {
|
||||||
renderEngine
|
if (location.path === '/') {
|
||||||
.parseAndRender(template, host)
|
host.use_default_location = false;
|
||||||
.then((config_text) => {
|
}
|
||||||
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
|
});
|
||||||
debug(logger, "Wrote config:", filename, config_text);
|
|
||||||
|
|
||||||
// Restore locations array
|
} else {
|
||||||
host.locations = origLocations;
|
locationsPromise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
resolve(true);
|
// Set the IPv6 setting for the host
|
||||||
})
|
host.ipv6 = internalNginx.ipv6Enabled();
|
||||||
.catch((err) => {
|
|
||||||
debug(logger, `Could not write ${filename}:`, err.message);
|
return locationsPromise.then(() => {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
renderEngine
|
||||||
});
|
.parseAndRender(template, host)
|
||||||
|
.then((config_text) => {
|
||||||
|
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
||||||
|
|
||||||
|
if (debug_mode) {
|
||||||
|
logger.success('Wrote config:', filename, config_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore locations array
|
||||||
|
host.locations = origLocations;
|
||||||
|
|
||||||
|
return true;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
debug(logger, `Could not render locations for ${filename}:`, err.message);
|
if (debug_mode) {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
logger.warn('Could not write ' + filename + ':', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new error.ConfigurationError(err.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* The templates rely on the certificate and access list relations being loaded.
|
|
||||||
* Not every caller fetches them (or fetches them fully), and when they're missing
|
|
||||||
* the config silently renders without SSL or without any access rules (#5919).
|
|
||||||
* Load anything that is referenced but not loaded onto the given host object.
|
|
||||||
*
|
|
||||||
* @param {String} host_type
|
|
||||||
* @param {Object} host
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
loadRenderRelations: async (host_type, host) => {
|
|
||||||
if (host.certificate_id > 0 && host.certificate?.id !== host.certificate_id) {
|
|
||||||
const certificate = await certificateModel
|
|
||||||
.query()
|
|
||||||
.where("is_deleted", 0)
|
|
||||||
.andWhere("id", host.certificate_id)
|
|
||||||
.first();
|
|
||||||
host.certificate = certificate ? JSON.parse(JSON.stringify(certificate)) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host_type === "proxy_host" && host.access_list_id > 0) {
|
|
||||||
if (
|
|
||||||
host.access_list?.id !== host.access_list_id ||
|
|
||||||
!Array.isArray(host.access_list.clients) ||
|
|
||||||
!Array.isArray(host.access_list.items)
|
|
||||||
) {
|
|
||||||
const accessList = await accessListModel
|
|
||||||
.query()
|
|
||||||
.where("is_deleted", 0)
|
|
||||||
.andWhere("id", host.access_list_id)
|
|
||||||
.withGraphFetched("[clients,items]")
|
|
||||||
.first();
|
|
||||||
host.access_list = accessList ? JSON.parse(JSON.stringify(accessList)) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!host.access_list) {
|
|
||||||
// _access.conf renders "deny all" when the list is missing, so fail closed
|
|
||||||
logger.error(
|
|
||||||
`Proxy Host #${host.id} references Access List #${host.access_list_id} which does not exist, all access will be denied`,
|
|
||||||
);
|
|
||||||
} else if (!host.access_list.clients.length && !host.access_list.items.length) {
|
|
||||||
logger.warn(
|
|
||||||
`Proxy Host #${host.id} uses Access List #${host.access_list_id} which has no clients or authorizations, no access restrictions will apply`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This generates a temporary nginx config listening on port 80 for the domain names listed
|
* 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
|
* in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt
|
||||||
@@ -400,26 +341,22 @@ const internalNginx = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
generateLetsEncryptRequestConfig: (certificate) => {
|
generateLetsEncryptRequestConfig: (certificate) => {
|
||||||
debug(logger, "Generating LetsEncrypt Request Config:", certificate);
|
if (debug_mode) {
|
||||||
const renderEngine = utils.getRenderEngine();
|
logger.info('Generating LetsEncrypt Request Config:', certificate);
|
||||||
|
|
||||||
const unsafe = (certificate.domain_names || []).find((name) => !isTokenSafe(name));
|
|
||||||
if (typeof unsafe !== "undefined") {
|
|
||||||
return Promise.reject(
|
|
||||||
unsafeValueError(
|
|
||||||
`Certificate #${certificate.id}: domain name ${JSON.stringify(unsafe)} is not allowed`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let renderEngine = new Liquid({
|
||||||
|
root: __dirname + '/../templates/'
|
||||||
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let template = null;
|
let template = null;
|
||||||
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
|
let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" });
|
template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
reject(new error.ConfigurationError(err.message));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,75 +365,51 @@ const internalNginx = {
|
|||||||
renderEngine
|
renderEngine
|
||||||
.parseAndRender(template, certificate)
|
.parseAndRender(template, certificate)
|
||||||
.then((config_text) => {
|
.then((config_text) => {
|
||||||
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
|
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
|
||||||
debug(logger, "Wrote config:", filename, config_text);
|
|
||||||
|
if (debug_mode) {
|
||||||
|
logger.success('Wrote config:', filename, config_text);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
debug(logger, `Could not write ${filename}:`, err.message);
|
if (debug_mode) {
|
||||||
reject(new errs.ConfigurationError(err.message));
|
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) => {
|
|
||||||
if (!fs.existsSync(filename)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
debug(logger, `Deleting file: ${filename}`);
|
|
||||||
fs.unlinkSync(filename);
|
|
||||||
} catch (err) {
|
|
||||||
debug(logger, "Could not delete file:", JSON.stringify(err, null, 2));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {String} host_type
|
|
||||||
* @returns String
|
|
||||||
*/
|
|
||||||
getFileFriendlyHostType: (host_type) => {
|
|
||||||
return host_type.replace(/-/g, "_");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`
|
* This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig`
|
||||||
*
|
*
|
||||||
* @param {Object} certificate
|
* @param {Object} certificate
|
||||||
|
* @param {Boolean} [throw_errors]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
deleteLetsEncryptRequestConfig: (certificate) => {
|
deleteLetsEncryptRequestConfig: (certificate, throw_errors) => {
|
||||||
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
|
return new Promise((resolve, reject) => {
|
||||||
return new Promise((resolve /*, reject*/) => {
|
try {
|
||||||
internalNginx.deleteFile(config_file);
|
let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
if (debug_mode) {
|
||||||
* @param {String} host_type
|
logger.warn('Deleting nginx config: ' + config_file);
|
||||||
* @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*/) => {
|
fs.unlinkSync(config_file);
|
||||||
internalNginx.deleteFile(config_file);
|
} catch (err) {
|
||||||
if (delete_err_file) {
|
if (debug_mode) {
|
||||||
internalNginx.deleteFile(config_file_err);
|
logger.warn('Could not delete config:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throw_errors) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -504,36 +417,44 @@ const internalNginx = {
|
|||||||
/**
|
/**
|
||||||
* @param {String} host_type
|
* @param {String} host_type
|
||||||
* @param {Object} [host]
|
* @param {Object} [host]
|
||||||
|
* @param {Boolean} [throw_errors]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
renameConfigAsError: (host_type, host) => {
|
deleteConfig: (host_type, host, throw_errors) => {
|
||||||
const config_file = internalNginx.getConfigName(
|
host_type = host_type.replace(new RegExp('-', 'g'), '_');
|
||||||
internalNginx.getFileFriendlyHostType(host_type),
|
|
||||||
typeof host === "undefined" ? 0 : host.id,
|
|
||||||
);
|
|
||||||
const config_file_err = `${config_file}.err`;
|
|
||||||
|
|
||||||
return new Promise((resolve /*, reject*/) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.unlink(config_file_err, () => {
|
try {
|
||||||
// ignore result, a previous .err file may not exist
|
let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
|
||||||
fs.rename(config_file, config_file_err, () => {
|
|
||||||
// also ignore result, as this is a debugging informative file anyway
|
if (debug_mode) {
|
||||||
resolve();
|
logger.warn('Deleting nginx config: ' + config_file);
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
fs.unlinkSync(config_file);
|
||||||
|
} catch (err) {
|
||||||
|
if (debug_mode) {
|
||||||
|
logger.warn('Could not delete config:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (throw_errors) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {String} hostType
|
* @param {String} host_type
|
||||||
* @param {Array} hosts
|
* @param {Array} hosts
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
bulkGenerateConfigs: (hostType, hosts) => {
|
bulkGenerateConfigs: (host_type, hosts) => {
|
||||||
const promises = [];
|
let promises = [];
|
||||||
hosts.map((host) => {
|
hosts.map(function (host) {
|
||||||
promises.push(internalNginx.generateConfig(hostType, host));
|
promises.push(internalNginx.generateConfig(host_type, host));
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
@@ -542,13 +463,13 @@ const internalNginx = {
|
|||||||
/**
|
/**
|
||||||
* @param {String} host_type
|
* @param {String} host_type
|
||||||
* @param {Array} hosts
|
* @param {Array} hosts
|
||||||
|
* @param {Boolean} [throw_errors]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
bulkDeleteConfigs: (host_type, hosts) => {
|
bulkDeleteConfigs: (host_type, hosts, throw_errors) => {
|
||||||
const promises = [];
|
let promises = [];
|
||||||
hosts.map((host) => {
|
hosts.map(function (host) {
|
||||||
promises.push(internalNginx.deleteConfig(host_type, host, true));
|
promises.push(internalNginx.deleteConfig(host_type, host, throw_errors));
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
@@ -558,19 +479,48 @@ const internalNginx = {
|
|||||||
* @param {string} config
|
* @param {string} config
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im),
|
advancedConfigHasDefaultLocation: function (config) {
|
||||||
|
return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
ipv6Enabled: () => {
|
ipv6Enabled: function () {
|
||||||
if (typeof process.env.DISABLE_IPV6 !== "undefined") {
|
if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
|
||||||
const disabled = process.env.DISABLE_IPV6.toLowerCase();
|
const disabled = process.env.DISABLE_IPV6.toLowerCase();
|
||||||
return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes");
|
return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
sslPassthroughEnabled: function () {
|
||||||
|
if (typeof process.env.ENABLE_SSL_PASSTHROUGH !== 'undefined') {
|
||||||
|
const enabled = process.env.ENABLE_SSL_PASSTHROUGH.toLowerCase();
|
||||||
|
return (enabled === 'on' || enabled === 'true' || enabled === '1' || enabled === 'yes');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to add brackets to an IP if it is IPv6
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
addIpv6Brackets: function (ip) {
|
||||||
|
// Only run check if ipv6 is enabled
|
||||||
|
if (internalNginx.ipv6Enabled()) {
|
||||||
|
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/gi;
|
||||||
|
if (ipv6Regex.test(ip)){
|
||||||
|
return `[${ip}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalNginx;
|
module.exports = internalNginx;
|
||||||
+229
-236
@@ -1,108 +1,99 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const proxyHostModel = require('../models/proxy_host');
|
||||||
import utils from "../lib/utils.js";
|
const internalHost = require('./host');
|
||||||
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
|
const internalNginx = require('./nginx');
|
||||||
import internalAuditLog from "./audit-log.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import internalCertificate from "./certificate.js";
|
const internalCertificate = require('./certificate');
|
||||||
import internalHost from "./host.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted", "owner.is_deleted"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
const internalProxyHost = {
|
const internalProxyHost = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: (access, data) => {
|
create: (access, data) => {
|
||||||
let thisData = data;
|
let create_certificate = data.certificate_id === 'new';
|
||||||
const createCertificate = thisData.certificate_id === "new";
|
|
||||||
|
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
delete thisData.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
return access.can('proxy_hosts:create', data)
|
||||||
.can("proxy_hosts:create", thisData)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Get a list of the domain names and check each of them against existing records
|
// Get a list of the domain names and check each of them against existing records
|
||||||
const domain_name_check_promises = [];
|
let domain_name_check_promises = [];
|
||||||
|
|
||||||
thisData.domain_names.map((domain_name) => {
|
data.domain_names.map(function (domain_name) {
|
||||||
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(domain_name_check_promises).then((check_results) => {
|
return Promise.all(domain_name_check_promises)
|
||||||
check_results.map((result) => {
|
.then((check_results) => {
|
||||||
if (result.is_taken) {
|
check_results.map(function (result) {
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
if (result.is_taken) {
|
||||||
}
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
return true;
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// At this point the domains should have been checked
|
// At this point the domains should have been checked
|
||||||
thisData.owner_user_id = access.token.getUserId(1);
|
data.owner_user_id = access.token.getUserId(1);
|
||||||
thisData = internalHost.cleanSslHstsData(thisData);
|
data = internalHost.cleanSslHstsData(data);
|
||||||
|
|
||||||
// Fix for db field not having a default value
|
return proxyHostModel
|
||||||
// for this optional field.
|
.query()
|
||||||
if (typeof thisData.advanced_config === "undefined") {
|
.omit(omissions())
|
||||||
thisData.advanced_config = "";
|
.insertAndFetch(data);
|
||||||
}
|
|
||||||
|
|
||||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
|
||||||
|
|
||||||
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
return internalCertificate
|
return internalCertificate.createQuickCertificate(access, data)
|
||||||
.createQuickCertificate(access, thisData)
|
|
||||||
.then((cert) => {
|
.then((cert) => {
|
||||||
// update host with cert id
|
// update host with cert id
|
||||||
return internalProxyHost.update(access, {
|
return internalProxyHost.update(access, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
certificate_id: cert.id,
|
certificate_id: cert.id
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// re-fetch with cert
|
// re-fetch with cert
|
||||||
return internalProxyHost.get(access, {
|
return internalProxyHost.get(access, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
expand: ["certificate", "owner", "access_list.[clients,items]"],
|
expand: ['certificate', 'owner', 'access_list.[clients,items]']
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(async (row) => {
|
|
||||||
// Configure nginx
|
|
||||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
|
|
||||||
return row;
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.then((row) => {
|
||||||
|
// Configure nginx
|
||||||
|
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
|
||||||
|
.then(() => {
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// Audit log
|
// Audit log
|
||||||
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
|
data.meta = _.assign({}, data.meta || {}, row.meta);
|
||||||
|
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'created',
|
||||||
action: "created",
|
object_type: 'proxy-host',
|
||||||
object_type: "proxy-host",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: thisData,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
@@ -116,113 +107,99 @@ const internalProxyHost = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
let thisData = data;
|
let create_certificate = data.certificate_id === 'new';
|
||||||
const createCertificate = thisData.certificate_id === "new";
|
|
||||||
|
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
delete thisData.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
return access.can('proxy_hosts:update', data.id)
|
||||||
.can("proxy_hosts:update", thisData.id)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// Get a list of the domain names and check each of them against existing records
|
// Get a list of the domain names and check each of them against existing records
|
||||||
const domain_name_check_promises = [];
|
let domain_name_check_promises = [];
|
||||||
|
|
||||||
if (typeof thisData.domain_names !== "undefined") {
|
if (typeof data.domain_names !== 'undefined') {
|
||||||
thisData.domain_names.map((domain_name) => {
|
data.domain_names.map(function (domain_name) {
|
||||||
return domain_name_check_promises.push(
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id));
|
||||||
internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(domain_name_check_promises).then((check_results) => {
|
return Promise.all(domain_name_check_promises)
|
||||||
check_results.map((result) => {
|
.then((check_results) => {
|
||||||
if (result.is_taken) {
|
check_results.map(function (result) {
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
if (result.is_taken) {
|
||||||
}
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
return true;
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalProxyHost.get(access, { id: thisData.id });
|
return internalProxyHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row.id !== thisData.id) {
|
if (row.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
||||||
`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before the certificate is created, so a refused request doesn't leave one behind
|
if (create_certificate) {
|
||||||
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
|
return internalCertificate.createQuickCertificate(access, {
|
||||||
|
domain_names: data.domain_names || row.domain_names,
|
||||||
if (createCertificate) {
|
meta: _.assign({}, row.meta, data.meta)
|
||||||
return internalCertificate
|
})
|
||||||
.createQuickCertificate(access, {
|
|
||||||
domain_names: thisData.domain_names || row.domain_names,
|
|
||||||
meta: _.assign({}, row.meta, thisData.meta),
|
|
||||||
})
|
|
||||||
.then((cert) => {
|
.then((cert) => {
|
||||||
// update host with cert id
|
// update host with cert id
|
||||||
thisData.certificate_id = cert.id;
|
data.certificate_id = cert.id;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
})
|
})
|
||||||
.then((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.
|
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
|
||||||
thisData = _.assign(
|
data = _.assign({}, {
|
||||||
{},
|
domain_names: row.domain_names
|
||||||
{
|
}, data);
|
||||||
domain_names: row.domain_names,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
|
|
||||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
data = internalHost.cleanSslHstsData(data, row);
|
||||||
|
|
||||||
return proxyHostModel
|
return proxyHostModel
|
||||||
.query()
|
.query()
|
||||||
.where({ id: thisData.id })
|
.where({id: data.id})
|
||||||
.patch(thisData)
|
.patch(data)
|
||||||
.then(utils.omitRow(omissions()))
|
|
||||||
.then((saved_row) => {
|
.then((saved_row) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'updated',
|
||||||
action: "updated",
|
object_type: 'proxy-host',
|
||||||
object_type: "proxy-host",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: thisData,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return saved_row;
|
return _.omit(saved_row, omissions());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalProxyHost
|
return internalProxyHost.get(access, {
|
||||||
.get(access, {
|
id: data.id,
|
||||||
id: thisData.id,
|
expand: ['owner', 'certificate', 'access_list.[clients,items]']
|
||||||
expand: ["owner", "certificate", "access_list.[clients,items]"],
|
})
|
||||||
})
|
.then((row) => {
|
||||||
.then(async (row) => {
|
|
||||||
if (!row.enabled) {
|
if (!row.enabled) {
|
||||||
// No need to add nginx config if host is disabled
|
// No need to add nginx config if host is disabled
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
|
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
|
||||||
row.meta = new_meta;
|
.then((new_meta) => {
|
||||||
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
|
row.meta = new_meta;
|
||||||
});
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
|
return _.omit(row, omissions());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -236,37 +213,41 @@ const internalProxyHost = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: (access, data) => {
|
get: (access, data) => {
|
||||||
const thisData = data || {};
|
if (typeof data === 'undefined') {
|
||||||
return access
|
data = {};
|
||||||
.can("proxy_hosts:get", thisData.id)
|
}
|
||||||
|
|
||||||
|
return access.can('proxy_hosts:get', data.id)
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const query = proxyHostModel
|
let query = proxyHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere("id", thisData.id)
|
.andWhere('id', data.id)
|
||||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
.allowEager('[owner,access_list,access_list.[clients,items],certificate]')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (access_data.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
// Custom omissions
|
||||||
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRow(omissions()));
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (row) {
|
||||||
throw new errs.ItemNotFoundError(thisData.id);
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
|
return _.omit(row, omissions());
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
const thisRow = internalHost.cleanRowCertificateMeta(row);
|
|
||||||
// Custom omissions
|
|
||||||
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
|
|
||||||
return _.omit(row, thisData.omit);
|
|
||||||
}
|
|
||||||
return thisRow;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -278,35 +259,35 @@ const internalProxyHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: (access, data) => {
|
delete: (access, data) => {
|
||||||
return access
|
return access.can('proxy_hosts:delete', data.id)
|
||||||
.can("proxy_hosts:delete", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalProxyHost.get(access, { id: data.id });
|
return internalProxyHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxyHostModel
|
return proxyHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
is_deleted: 1,
|
is_deleted: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("proxy_host", row).then(() => {
|
return internalNginx.deleteConfig('proxy_host', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "deleted",
|
action: 'deleted',
|
||||||
object_type: "proxy-host",
|
object_type: 'proxy-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -323,39 +304,43 @@ const internalProxyHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
enable: (access, data) => {
|
enable: (access, data) => {
|
||||||
return access
|
return access.can('proxy_hosts:update', data.id)
|
||||||
.can("proxy_hosts:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalProxyHost.get(access, {
|
return internalProxyHost.get(access, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
expand: ["certificate", "owner", "access_list.[clients,items]"],
|
expand: ['certificate', 'owner', 'access_list']
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(async (row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (row.enabled) {
|
||||||
if (row.enabled) {
|
throw new error.ValidationError('Host is already enabled');
|
||||||
throw new errs.ValidationError("Host is already enabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 1;
|
row.enabled = 1;
|
||||||
|
|
||||||
await proxyHostModel.query().where("id", row.id).patch({
|
return proxyHostModel
|
||||||
enabled: 1,
|
.query()
|
||||||
});
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
// Configure nginx
|
enabled: 1
|
||||||
await internalNginx.configure(proxyHostModel, "proxy_host", row);
|
})
|
||||||
|
.then(() => {
|
||||||
// Add to audit log
|
// Configure nginx
|
||||||
await internalAuditLog.add(access, {
|
return internalNginx.configure(proxyHostModel, 'proxy_host', row);
|
||||||
action: "enabled",
|
})
|
||||||
object_type: "proxy-host",
|
.then(() => {
|
||||||
object_id: row.id,
|
// Add to audit log
|
||||||
meta: _.omit(row, omissions()),
|
return internalAuditLog.add(access, {
|
||||||
});
|
action: 'enabled',
|
||||||
|
object_type: 'proxy-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: _.omit(row, omissions())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -368,40 +353,39 @@ const internalProxyHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
disable: (access, data) => {
|
disable: (access, data) => {
|
||||||
return access
|
return access.can('proxy_hosts:update', data.id)
|
||||||
.can("proxy_hosts:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalProxyHost.get(access, { id: data.id });
|
return internalProxyHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (!row.enabled) {
|
||||||
if (!row.enabled) {
|
throw new error.ValidationError('Host is already disabled');
|
||||||
throw new errs.ValidationError("Host is already disabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 0;
|
row.enabled = 0;
|
||||||
|
|
||||||
return proxyHostModel
|
return proxyHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
enabled: 0,
|
enabled: 0
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("proxy_host", row).then(() => {
|
return internalNginx.deleteConfig('proxy_host', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "disabled",
|
action: 'disabled',
|
||||||
object_type: "proxy-host",
|
object_type: 'proxy-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -418,36 +402,41 @@ const internalProxyHost = {
|
|||||||
* @param {String} [search_query]
|
* @param {String} [search_query]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: async (access, expand, searchQuery) => {
|
getAll: (access, expand, search_query) => {
|
||||||
const accessData = await access.can("proxy_hosts:list");
|
return access.can('proxy_hosts:list')
|
||||||
|
.then((access_data) => {
|
||||||
|
let query = proxyHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.groupBy('id')
|
||||||
|
.omit(['is_deleted'])
|
||||||
|
.allowEager('[owner,access_list,certificate]')
|
||||||
|
.orderBy('domain_names', 'ASC');
|
||||||
|
|
||||||
const query = proxyHostModel
|
if (access_data.permission_visibility !== 'all') {
|
||||||
.query()
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
.where("is_deleted", 0)
|
}
|
||||||
.groupBy("id")
|
|
||||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
|
||||||
.orderBy(castJsonIfNeed("domain_names"), "ASC");
|
|
||||||
|
|
||||||
if (accessData.permission_visibility !== "all") {
|
// Query is used for searching
|
||||||
scopeToOwnerOrAllowed(query, access.token.getUserId(1), access.getPermissions?.()?.meta);
|
if (typeof search_query === 'string') {
|
||||||
}
|
query.where(function () {
|
||||||
|
this.where('domain_names', 'like', '%' + search_query + '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Query is used for searching
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
if (typeof searchQuery === "string" && searchQuery.length > 0) {
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
query.where(function () {
|
}
|
||||||
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then((rows) => {
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
|
||||||
|
return internalHost.cleanAllRowsCertificateMeta(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await query.then(utils.omitRows(omissions()));
|
|
||||||
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
|
|
||||||
return internalHost.cleanAllRowsCertificateMeta(rows);
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -455,19 +444,23 @@ const internalProxyHost = {
|
|||||||
*
|
*
|
||||||
* @param {Number} user_id
|
* @param {Number} user_id
|
||||||
* @param {String} visibility
|
* @param {String} visibility
|
||||||
* @param {Object} [permissionsMeta] the user's permissions meta, as already loaded by access
|
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getCount: async (user_id, visibility, permissionsMeta) => {
|
getCount: (user_id, visibility) => {
|
||||||
const query = proxyHostModel.query().count("id as count").where("is_deleted", 0);
|
let query = proxyHostModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0);
|
||||||
|
|
||||||
if (visibility !== "all") {
|
if (visibility !== 'all') {
|
||||||
scopeToOwnerOrAllowed(query, user_id, permissionsMeta);
|
query.andWhere('owner_user_id', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = await query.first();
|
return query.first()
|
||||||
return Number.parseInt(row.count, 10);
|
.then((row) => {
|
||||||
},
|
return parseInt(row.count, 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalProxyHost;
|
module.exports = internalProxyHost;
|
||||||
@@ -1,107 +1,98 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const redirectionHostModel = require('../models/redirection_host');
|
||||||
import utils from "../lib/utils.js";
|
const internalHost = require('./host');
|
||||||
import redirectionHostModel from "../models/redirection_host.js";
|
const internalNginx = require('./nginx');
|
||||||
import internalAuditLog from "./audit-log.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import internalCertificate from "./certificate.js";
|
const internalCertificate = require('./certificate');
|
||||||
import internalHost from "./host.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
const internalRedirectionHost = {
|
const internalRedirectionHost = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: (access, data) => {
|
create: (access, data) => {
|
||||||
let thisData = data || {};
|
let create_certificate = data.certificate_id === 'new';
|
||||||
const createCertificate = thisData.certificate_id === "new";
|
|
||||||
|
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
delete thisData.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
return access.can('redirection_hosts:create', data)
|
||||||
.can("redirection_hosts:create", thisData)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// Get a list of the domain names and check each of them against existing records
|
// Get a list of the domain names and check each of them against existing records
|
||||||
const domain_name_check_promises = [];
|
let domain_name_check_promises = [];
|
||||||
|
|
||||||
thisData.domain_names.map((domain_name) => {
|
data.domain_names.map(function (domain_name) {
|
||||||
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(domain_name_check_promises).then((check_results) => {
|
return Promise.all(domain_name_check_promises)
|
||||||
check_results.map((result) => {
|
.then((check_results) => {
|
||||||
if (result.is_taken) {
|
check_results.map(function (result) {
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
if (result.is_taken) {
|
||||||
}
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
return true;
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// At this point the domains should have been checked
|
// At this point the domains should have been checked
|
||||||
thisData.owner_user_id = access.token.getUserId(1);
|
data.owner_user_id = access.token.getUserId(1);
|
||||||
thisData = internalHost.cleanSslHstsData(thisData);
|
data = internalHost.cleanSslHstsData(data);
|
||||||
|
|
||||||
// Fix for db field not having a default value
|
return redirectionHostModel
|
||||||
// for this optional field.
|
.query()
|
||||||
if (typeof thisData.advanced_config === "undefined") {
|
.omit(omissions())
|
||||||
thisData.advanced_config = "";
|
.insertAndFetch(data);
|
||||||
}
|
|
||||||
|
|
||||||
internalHost.assertAdvancedConfigAllowed(access, thisData);
|
|
||||||
|
|
||||||
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
|
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
return internalCertificate
|
return internalCertificate.createQuickCertificate(access, data)
|
||||||
.createQuickCertificate(access, thisData)
|
|
||||||
.then((cert) => {
|
.then((cert) => {
|
||||||
// update host with cert id
|
// update host with cert id
|
||||||
return internalRedirectionHost.update(access, {
|
return internalRedirectionHost.update(access, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
certificate_id: cert.id,
|
certificate_id: cert.id
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// re-fetch with cert
|
// re-fetch with cert
|
||||||
return internalRedirectionHost.get(access, {
|
return internalRedirectionHost.get(access, {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
expand: ["certificate", "owner"],
|
expand: ['certificate', 'owner']
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => {
|
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
|
||||||
return row;
|
.then(() => {
|
||||||
});
|
return row;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
|
data.meta = _.assign({}, data.meta || {}, row.meta);
|
||||||
|
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'created',
|
||||||
action: "created",
|
object_type: 'redirection-host',
|
||||||
object_type: "redirection-host",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: thisData,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
@@ -115,110 +106,94 @@ const internalRedirectionHost = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
let thisData = data || {};
|
let create_certificate = data.certificate_id === 'new';
|
||||||
const createCertificate = thisData.certificate_id === "new";
|
|
||||||
|
|
||||||
if (createCertificate) {
|
if (create_certificate) {
|
||||||
delete thisData.certificate_id;
|
delete data.certificate_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
return access.can('redirection_hosts:update', data.id)
|
||||||
.can("redirection_hosts:update", thisData.id)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// Get a list of the domain names and check each of them against existing records
|
// Get a list of the domain names and check each of them against existing records
|
||||||
const domain_name_check_promises = [];
|
let domain_name_check_promises = [];
|
||||||
|
|
||||||
if (typeof thisData.domain_names !== "undefined") {
|
if (typeof data.domain_names !== 'undefined') {
|
||||||
thisData.domain_names.map((domain_name) => {
|
data.domain_names.map(function (domain_name) {
|
||||||
domain_name_check_promises.push(
|
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id));
|
||||||
internalHost.isHostnameTaken(domain_name, "redirection", thisData.id),
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(domain_name_check_promises).then((check_results) => {
|
return Promise.all(domain_name_check_promises)
|
||||||
check_results.map((result) => {
|
.then((check_results) => {
|
||||||
if (result.is_taken) {
|
check_results.map(function (result) {
|
||||||
throw new errs.ValidationError(`${result.hostname} is already in use`);
|
if (result.is_taken) {
|
||||||
}
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
return true;
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalRedirectionHost.get(access, { id: thisData.id });
|
return internalRedirectionHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row.id !== thisData.id) {
|
if (row.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
||||||
`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before the certificate is created, so a refused request doesn't leave one behind
|
if (create_certificate) {
|
||||||
internalHost.assertAdvancedConfigAllowed(access, thisData, row);
|
return internalCertificate.createQuickCertificate(access, {
|
||||||
|
domain_names: data.domain_names || row.domain_names,
|
||||||
if (createCertificate) {
|
meta: _.assign({}, row.meta, data.meta)
|
||||||
return internalCertificate
|
})
|
||||||
.createQuickCertificate(access, {
|
|
||||||
domain_names: thisData.domain_names || row.domain_names,
|
|
||||||
meta: _.assign({}, row.meta, thisData.meta),
|
|
||||||
})
|
|
||||||
.then((cert) => {
|
.then((cert) => {
|
||||||
// update host with cert id
|
// update host with cert id
|
||||||
thisData.certificate_id = cert.id;
|
data.certificate_id = cert.id;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
})
|
})
|
||||||
.then((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.
|
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
|
||||||
thisData = _.assign(
|
data = _.assign({}, {
|
||||||
{},
|
domain_names: row.domain_names
|
||||||
{
|
}, data);
|
||||||
domain_names: row.domain_names,
|
|
||||||
},
|
|
||||||
thisData,
|
|
||||||
);
|
|
||||||
|
|
||||||
thisData = internalHost.cleanSslHstsData(thisData, row);
|
data = internalHost.cleanSslHstsData(data, row);
|
||||||
|
|
||||||
return redirectionHostModel
|
return redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where({ id: thisData.id })
|
.where({id: data.id})
|
||||||
.patch(thisData)
|
.patch(data)
|
||||||
.then((saved_row) => {
|
.then((saved_row) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'updated',
|
||||||
action: "updated",
|
object_type: 'redirection-host',
|
||||||
object_type: "redirection-host",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: thisData,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return _.omit(saved_row, omissions());
|
return _.omit(saved_row, omissions());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalRedirectionHost
|
return internalRedirectionHost.get(access, {
|
||||||
.get(access, {
|
id: data.id,
|
||||||
id: thisData.id,
|
expand: ['owner', 'certificate']
|
||||||
expand: ["owner", "certificate"],
|
})
|
||||||
})
|
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx
|
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
|
||||||
.configure(redirectionHostModel, "redirection_host", row)
|
|
||||||
.then((new_meta) => {
|
.then((new_meta) => {
|
||||||
row.meta = new_meta;
|
row.meta = new_meta;
|
||||||
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
|
return _.omit(row, omissions());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -233,38 +208,41 @@ const internalRedirectionHost = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: (access, data) => {
|
get: (access, data) => {
|
||||||
const thisData = data || {};
|
if (typeof data === 'undefined') {
|
||||||
return access
|
data = {};
|
||||||
.can("redirection_hosts:get", thisData.id)
|
}
|
||||||
|
|
||||||
|
return access.can('redirection_hosts:get', data.id)
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const query = redirectionHostModel
|
let query = redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere("id", thisData.id)
|
.andWhere('id', data.id)
|
||||||
.allowGraph(redirectionHostModel.defaultAllowGraph)
|
.allowEager('[owner,certificate]')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (access_data.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
// Custom omissions
|
||||||
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRow(omissions()));
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
let thisRow = row;
|
if (row) {
|
||||||
if (!thisRow?.id) {
|
row = internalHost.cleanRowCertificateMeta(row);
|
||||||
throw new errs.ItemNotFoundError(thisData.id);
|
return _.omit(row, omissions());
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
|
|
||||||
// Custom omissions
|
|
||||||
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
|
|
||||||
return _.omit(thisRow, thisData.omit);
|
|
||||||
}
|
|
||||||
return thisRow;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -276,35 +254,35 @@ const internalRedirectionHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: (access, data) => {
|
delete: (access, data) => {
|
||||||
return access
|
return access.can('redirection_hosts:delete', data.id)
|
||||||
.can("redirection_hosts:delete", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalRedirectionHost.get(access, { id: data.id });
|
return internalRedirectionHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirectionHostModel
|
return redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
is_deleted: 1,
|
is_deleted: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("redirection_host", row).then(() => {
|
return internalNginx.deleteConfig('redirection_host', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "deleted",
|
action: 'deleted',
|
||||||
object_type: "redirection-host",
|
object_type: 'redirection-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -321,41 +299,39 @@ const internalRedirectionHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
enable: (access, data) => {
|
enable: (access, data) => {
|
||||||
return access
|
return access.can('redirection_hosts:update', data.id)
|
||||||
.can("redirection_hosts:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalRedirectionHost.get(access, {
|
return internalRedirectionHost.get(access, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
expand: ["certificate", "owner"],
|
expand: ['certificate', 'owner']
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (row.enabled) {
|
||||||
if (row.enabled) {
|
throw new error.ValidationError('Host is already enabled');
|
||||||
throw new errs.ValidationError("Host is already enabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 1;
|
row.enabled = 1;
|
||||||
|
|
||||||
return redirectionHostModel
|
return redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
enabled: 1,
|
enabled: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(redirectionHostModel, "redirection_host", row);
|
return internalNginx.configure(redirectionHostModel, 'redirection_host', row);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "enabled",
|
action: 'enabled',
|
||||||
object_type: "redirection-host",
|
object_type: 'redirection-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -372,40 +348,39 @@ const internalRedirectionHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
disable: (access, data) => {
|
disable: (access, data) => {
|
||||||
return access
|
return access.can('redirection_hosts:update', data.id)
|
||||||
.can("redirection_hosts:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalRedirectionHost.get(access, { id: data.id });
|
return internalRedirectionHost.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (!row.enabled) {
|
||||||
if (!row.enabled) {
|
throw new error.ValidationError('Host is already disabled');
|
||||||
throw new errs.ValidationError("Host is already disabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 0;
|
row.enabled = 0;
|
||||||
|
|
||||||
return redirectionHostModel
|
return redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
enabled: 0,
|
enabled: 0
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("redirection_host", row).then(() => {
|
return internalNginx.deleteConfig('redirection_host', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "disabled",
|
action: 'disabled',
|
||||||
object_type: "redirection-host",
|
object_type: 'redirection-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -423,35 +398,35 @@ const internalRedirectionHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: (access, expand, search_query) => {
|
getAll: (access, expand, search_query) => {
|
||||||
return access
|
return access.can('redirection_hosts:list')
|
||||||
.can("redirection_hosts:list")
|
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const query = redirectionHostModel
|
let query = redirectionHostModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.groupBy("id")
|
.groupBy('id')
|
||||||
.allowGraph(redirectionHostModel.defaultAllowGraph)
|
.omit(['is_deleted'])
|
||||||
.orderBy(castJsonIfNeed("domain_names"), "ASC");
|
.allowEager('[owner,certificate]')
|
||||||
|
.orderBy('domain_names', 'ASC');
|
||||||
|
|
||||||
if (access_data.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query is used for searching
|
// Query is used for searching
|
||||||
if (typeof search_query === "string" && search_query.length > 0) {
|
if (typeof search_query === 'string') {
|
||||||
query.where(function () {
|
query.where(function () {
|
||||||
this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
|
this.where('domain_names', 'like', '%' + search_query + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRows(omissions()));
|
return query;
|
||||||
})
|
})
|
||||||
.then((rows) => {
|
.then((rows) => {
|
||||||
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
|
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
|
||||||
return internalHost.cleanAllRowsCertificateMeta(rows);
|
return internalHost.cleanAllRowsCertificateMeta(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,16 +442,20 @@ const internalRedirectionHost = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getCount: (user_id, visibility) => {
|
getCount: (user_id, visibility) => {
|
||||||
const query = redirectionHostModel.query().count("id as count").where("is_deleted", 0);
|
let query = redirectionHostModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0);
|
||||||
|
|
||||||
if (visibility !== "all") {
|
if (visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", user_id);
|
query.andWhere('owner_user_id', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.first().then((row) => {
|
return query.first()
|
||||||
return Number.parseInt(row.count, 10);
|
.then((row) => {
|
||||||
});
|
return parseInt(row.count, 10);
|
||||||
},
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalRedirectionHost;
|
module.exports = internalRedirectionHost;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import https from "node:https";
|
|
||||||
import { ProxyAgent } from "proxy-agent";
|
|
||||||
import { debug, remoteVersion as logger } from "../logger.js";
|
|
||||||
import pjson from "../package.json" with { type: "json" };
|
|
||||||
|
|
||||||
const VERSION_URL = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
|
|
||||||
|
|
||||||
const internalRemoteVersion = {
|
|
||||||
cache_timeout: 1000 * 60 * 15, // 15 minutes
|
|
||||||
last_result: null,
|
|
||||||
last_fetch_time: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the latest version info, using a cached result if within the cache timeout period.
|
|
||||||
* @return {Promise<{current: string, latest: string, update_available: boolean}>} Version info
|
|
||||||
*/
|
|
||||||
get: async () => {
|
|
||||||
if (
|
|
||||||
!internalRemoteVersion.last_result ||
|
|
||||||
!internalRemoteVersion.last_fetch_time ||
|
|
||||||
Date.now() - internalRemoteVersion.last_fetch_time > internalRemoteVersion.cache_timeout
|
|
||||||
) {
|
|
||||||
const raw = await internalRemoteVersion.fetchUrl(VERSION_URL);
|
|
||||||
const data = JSON.parse(raw);
|
|
||||||
internalRemoteVersion.last_result = data;
|
|
||||||
internalRemoteVersion.last_fetch_time = Date.now();
|
|
||||||
} else {
|
|
||||||
debug(logger, "Using cached remote version result");
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = internalRemoteVersion.last_result.tag_name;
|
|
||||||
const version = pjson.version.split("-").shift().split(".");
|
|
||||||
const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
|
|
||||||
return {
|
|
||||||
current: currentVersion,
|
|
||||||
latest: latestVersion,
|
|
||||||
update_available: internalRemoteVersion.compareVersions(currentVersion, latestVersion),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchUrl: (url) => {
|
|
||||||
const agent = new ProxyAgent();
|
|
||||||
const headers = {
|
|
||||||
"User-Agent": `NginxProxyManager v${pjson.version}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
logger.info(`Fetching ${url}`);
|
|
||||||
return https
|
|
||||||
.get(url, { agent, headers }, (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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
compareVersions: (current, latest) => {
|
|
||||||
const cleanCurrent = current.replace(/^v/, "");
|
|
||||||
const cleanLatest = latest.replace(/^v/, "");
|
|
||||||
|
|
||||||
const currentParts = cleanCurrent.split(".").map(Number);
|
|
||||||
const latestParts = cleanLatest.split(".").map(Number);
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
||||||
const curr = currentParts[i] || 0;
|
|
||||||
const lat = latestParts[i] || 0;
|
|
||||||
|
|
||||||
if (lat > curr) return true;
|
|
||||||
if (lat < curr) return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default internalRemoteVersion;
|
|
||||||
+18
-21
@@ -1,41 +1,38 @@
|
|||||||
import internalDeadHost from "./dead-host.js";
|
const internalProxyHost = require('./proxy-host');
|
||||||
import internalProxyHost from "./proxy-host.js";
|
const internalRedirectionHost = require('./redirection-host');
|
||||||
import internalRedirectionHost from "./redirection-host.js";
|
const internalDeadHost = require('./dead-host');
|
||||||
import internalStream from "./stream.js";
|
const internalStream = require('./stream');
|
||||||
|
|
||||||
const internalReport = {
|
const internalReport = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
getHostsReport: (access) => {
|
getHostsReport: (access) => {
|
||||||
return access
|
return access.can('reports:hosts', 1)
|
||||||
.can("reports:hosts", 1)
|
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const userId = access.token.getUserId(1);
|
let user_id = access.token.getUserId(1);
|
||||||
|
|
||||||
const promises = [
|
let promises = [
|
||||||
internalProxyHost.getCount(
|
internalProxyHost.getCount(user_id, access_data.visibility),
|
||||||
userId,
|
internalRedirectionHost.getCount(user_id, access_data.visibility),
|
||||||
access_data.permission_visibility,
|
internalStream.getCount(user_id, access_data.visibility),
|
||||||
access.getPermissions?.()?.meta,
|
internalDeadHost.getCount(user_id, access_data.visibility)
|
||||||
),
|
|
||||||
internalRedirectionHost.getCount(userId, access_data.permission_visibility),
|
|
||||||
internalStream.getCount(userId, access_data.permission_visibility),
|
|
||||||
internalDeadHost.getCount(userId, access_data.permission_visibility),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then((counts) => {
|
.then((counts) => {
|
||||||
return {
|
return {
|
||||||
proxy: counts.shift(),
|
proxy: counts.shift(),
|
||||||
redirection: counts.shift(),
|
redirection: counts.shift(),
|
||||||
stream: counts.shift(),
|
stream: counts.shift(),
|
||||||
dead: counts.shift(),
|
dead: counts.shift()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalReport;
|
module.exports = internalReport;
|
||||||
+43
-35
@@ -1,9 +1,10 @@
|
|||||||
import fs from "node:fs";
|
const fs = require('fs');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import settingModel from "../models/setting.js";
|
const settingModel = require('../models/setting');
|
||||||
import internalNginx from "./nginx.js";
|
const internalNginx = require('./nginx');
|
||||||
|
|
||||||
const internalSetting = {
|
const internalSetting = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
@@ -11,38 +12,37 @@ const internalSetting = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
return access
|
return access.can('settings:update', data.id)
|
||||||
.can("settings:update", data.id)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
return internalSetting.get(access, { id: data.id });
|
return internalSetting.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row.id !== data.id) {
|
if (row.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
||||||
`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return settingModel.query().where({ id: data.id }).patch(data);
|
return settingModel
|
||||||
|
.query()
|
||||||
|
.where({id: data.id})
|
||||||
|
.patch(data);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalSetting.get(access, {
|
return internalSetting.get(access, {
|
||||||
id: data.id,
|
id: data.id
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row.id === "default-site") {
|
if (row.id === 'default-site') {
|
||||||
// write the html if we need to
|
// write the html if we need to
|
||||||
if (row.value === "html") {
|
if (row.value === 'html') {
|
||||||
fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" });
|
fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx
|
return internalNginx.deleteConfig('default')
|
||||||
.deleteConfig("default")
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalNginx.generateConfig("default", row);
|
return internalNginx.generateConfig('default', row);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalNginx.test();
|
return internalNginx.test();
|
||||||
@@ -54,8 +54,7 @@ const internalSetting = {
|
|||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
.catch((/*err*/) => {
|
.catch((/*err*/) => {
|
||||||
internalNginx
|
internalNginx.deleteConfig('default')
|
||||||
.deleteConfig("default")
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalNginx.test();
|
return internalNginx.test();
|
||||||
})
|
})
|
||||||
@@ -64,11 +63,12 @@ const internalSetting = {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// I'm being slack here I know..
|
// I'm being slack here I know..
|
||||||
throw new errs.ValidationError("Could not reconfigure Nginx. Please check logs.");
|
throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
}
|
}
|
||||||
return row;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -79,16 +79,19 @@ const internalSetting = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: (access, data) => {
|
get: (access, data) => {
|
||||||
return access
|
return access.can('settings:get', data.id)
|
||||||
.can("settings:get", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return settingModel.query().where("id", data.id).first();
|
return settingModel
|
||||||
|
.query()
|
||||||
|
.where('id', data.id)
|
||||||
|
.first();
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
return row;
|
return row;
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -99,13 +102,15 @@ const internalSetting = {
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getCount: (access) => {
|
getCount: (access) => {
|
||||||
return access
|
return access.can('settings:list')
|
||||||
.can("settings:list")
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return settingModel.query().count("id as count").first();
|
return settingModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.first();
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
return Number.parseInt(row.count, 10);
|
return parseInt(row.count, 10);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -116,10 +121,13 @@ const internalSetting = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: (access) => {
|
getAll: (access) => {
|
||||||
return access.can("settings:list").then(() => {
|
return access.can('settings:list')
|
||||||
return settingModel.query().orderBy("description", "ASC");
|
.then(() => {
|
||||||
});
|
return settingModel
|
||||||
},
|
.query()
|
||||||
|
.orderBy('description', 'ASC');
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalSetting;
|
module.exports = internalSetting;
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const error = require('../lib/error');
|
||||||
|
const passthroughHostModel = require('../models/ssl_passthrough_host');
|
||||||
|
const internalHost = require('./host');
|
||||||
|
const internalNginx = require('./nginx');
|
||||||
|
const internalAuditLog = require('./audit-log');
|
||||||
|
|
||||||
|
function omissions () {
|
||||||
|
return ['is_deleted'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalPassthroughHost = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {Object} data
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
create: (access, data) => {
|
||||||
|
return access.can('ssl_passthrough_hosts:create', data)
|
||||||
|
.then(() => {
|
||||||
|
// Get the domain name and check it against existing records
|
||||||
|
return internalHost.isHostnameTaken(data.domain_name)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.is_taken) {
|
||||||
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then((/*access_data*/) => {
|
||||||
|
data.owner_user_id = access.token.getUserId(1);
|
||||||
|
|
||||||
|
if (typeof data.meta === 'undefined') {
|
||||||
|
data.meta = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.omit(omissions())
|
||||||
|
.insertAndFetch(data);
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
// Configure nginx
|
||||||
|
return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {})
|
||||||
|
.then(() => {
|
||||||
|
return internalPassthroughHost.get(access, {id: row.id, expand: ['owner']});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'created',
|
||||||
|
object_type: 'ssl-passthrough-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) => {
|
||||||
|
return access.can('ssl_passthrough_hosts:update', data.id)
|
||||||
|
.then((/*access_data*/) => {
|
||||||
|
// Get the domain name and check it against existing records
|
||||||
|
if (typeof data.domain_name !== 'undefined') {
|
||||||
|
return internalHost.isHostnameTaken(data.domain_name, 'ssl_passthrough', data.id)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.is_taken) {
|
||||||
|
throw new error.ValidationError(result.hostname + ' is already in use');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then((/*access_data*/) => {
|
||||||
|
return internalPassthroughHost.get(access, {id: data.id});
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (row.id !== data.id) {
|
||||||
|
// Sanity check that something crazy hasn't happened
|
||||||
|
throw new error.InternalValidationError('SSL Passthrough Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.omit(omissions())
|
||||||
|
.patchAndFetchById(row.id, data)
|
||||||
|
.then(() => {
|
||||||
|
return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {})
|
||||||
|
.then(() => {
|
||||||
|
return internalPassthroughHost.get(access, {id: row.id, expand: ['owner']});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((saved_row) => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'updated',
|
||||||
|
object_type: 'ssl-passthrough-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: data
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return _.omit(saved_row, omissions());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Number} data.id
|
||||||
|
* @param {Array} [data.expand]
|
||||||
|
* @param {Array} [data.omit]
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
get: (access, data) => {
|
||||||
|
if (typeof data === 'undefined') {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return access.can('ssl_passthrough_hosts:get', data.id)
|
||||||
|
.then((access_data) => {
|
||||||
|
let query = passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.andWhere('id', data.id)
|
||||||
|
.allowEager('[owner]')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (access_data.permission_visibility !== 'all') {
|
||||||
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom omissions
|
||||||
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (row) {
|
||||||
|
return _.omit(row, omissions());
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {Number} data.id
|
||||||
|
* @param {String} [data.reason]
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
delete: (access, data) => {
|
||||||
|
return access.can('ssl_passthrough_hosts:delete', data.id)
|
||||||
|
.then(() => {
|
||||||
|
return internalPassthroughHost.get(access, {id: data.id});
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (!row) {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
|
is_deleted: 1
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Update Nginx Config
|
||||||
|
return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {})
|
||||||
|
.then(() => {
|
||||||
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'deleted',
|
||||||
|
object_type: 'ssl-passthrough-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('ssl_passthrough_hosts:update', data.id)
|
||||||
|
.then(() => {
|
||||||
|
return internalPassthroughHost.get(access, {
|
||||||
|
id: data.id,
|
||||||
|
expand: ['owner']
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (!row) {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
} else if (row.enabled) {
|
||||||
|
throw new error.ValidationError('Host is already enabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
row.enabled = 1;
|
||||||
|
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
|
enabled: 1
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Configure nginx
|
||||||
|
return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'enabled',
|
||||||
|
object_type: 'ssl-passthrough-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('ssl_passthrough_hosts:update', data.id)
|
||||||
|
.then(() => {
|
||||||
|
return internalPassthroughHost.get(access, {id: data.id});
|
||||||
|
})
|
||||||
|
.then((row) => {
|
||||||
|
if (!row) {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
|
} else if (!row.enabled) {
|
||||||
|
throw new error.ValidationError('Host is already disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
row.enabled = 0;
|
||||||
|
|
||||||
|
return passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('id', row.id)
|
||||||
|
.patch({
|
||||||
|
enabled: 0
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Update Nginx Config
|
||||||
|
return internalNginx.configure(passthroughHostModel, 'ssl_passthrough_host', {})
|
||||||
|
.then(() => {
|
||||||
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Add to audit log
|
||||||
|
return internalAuditLog.add(access, {
|
||||||
|
action: 'disabled',
|
||||||
|
object_type: 'ssl-passthrough-host',
|
||||||
|
object_id: row.id,
|
||||||
|
meta: _.omit(row, omissions())
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All SSL Passthrough Hosts
|
||||||
|
*
|
||||||
|
* @param {Access} access
|
||||||
|
* @param {Array} [expand]
|
||||||
|
* @param {String} [search_query]
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getAll: (access, expand, search_query) => {
|
||||||
|
return access.can('ssl_passthrough_hosts:list')
|
||||||
|
.then((access_data) => {
|
||||||
|
let query = passthroughHostModel
|
||||||
|
.query()
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.groupBy('id')
|
||||||
|
.omit(['is_deleted'])
|
||||||
|
.allowEager('[owner]')
|
||||||
|
.orderBy('domain_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('domain_name', 'like', '%' + search_query + '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report use
|
||||||
|
*
|
||||||
|
* @param {Number} user_id
|
||||||
|
* @param {String} visibility
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getCount: (user_id, visibility) => {
|
||||||
|
let query = passthroughHostModel
|
||||||
|
.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 = internalPassthroughHost;
|
||||||
+137
-215
@@ -1,85 +1,50 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
const streamModel = require('../models/stream');
|
||||||
import utils from "../lib/utils.js";
|
const internalNginx = require('./nginx');
|
||||||
import streamModel from "../models/stream.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import internalAuditLog from "./audit-log.js";
|
|
||||||
import internalCertificate from "./certificate.js";
|
|
||||||
import internalHost from "./host.js";
|
|
||||||
import internalNginx from "./nginx.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
const internalStream = {
|
const internalStream = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: (access, data) => {
|
create: (access, data) => {
|
||||||
const create_certificate = data.certificate_id === "new";
|
return access.can('streams:create', data)
|
||||||
|
|
||||||
if (create_certificate) {
|
|
||||||
delete data.certificate_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return access
|
|
||||||
.can("streams:create", data)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// TODO: At this point the existing ports should have been checked
|
// TODO: At this point the existing ports should have been checked
|
||||||
data.owner_user_id = access.token.getUserId(1);
|
data.owner_user_id = access.token.getUserId(1);
|
||||||
|
|
||||||
if (typeof data.meta === "undefined") {
|
if (typeof data.meta === 'undefined') {
|
||||||
data.meta = {};
|
data.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// streams aren't routed by domain name so don't store domain names in the DB
|
return streamModel
|
||||||
const data_no_domains = structuredClone(data);
|
.query()
|
||||||
delete data_no_domains.domain_names;
|
.omit(omissions())
|
||||||
|
.insertAndFetch(data);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
})
|
|
||||||
.then((row) => {
|
|
||||||
// re-fetch with cert
|
|
||||||
return internalStream.get(access, {
|
|
||||||
id: row.id,
|
|
||||||
expand: ["certificate", "owner"],
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(streamModel, "stream", row).then(() => {
|
return internalNginx.configure(streamModel, 'stream', row)
|
||||||
return row;
|
.then(() => {
|
||||||
});
|
return internalStream.get(access, {id: row.id, expand: ['owner']});
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'created',
|
||||||
action: "created",
|
object_type: 'stream',
|
||||||
object_type: "stream",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: data,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
@@ -93,78 +58,39 @@ const internalStream = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
let thisData = data;
|
return access.can('streams:update', data.id)
|
||||||
const create_certificate = thisData.certificate_id === "new";
|
|
||||||
|
|
||||||
if (create_certificate) {
|
|
||||||
delete thisData.certificate_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return access
|
|
||||||
.can("streams:update", thisData.id)
|
|
||||||
.then((/*access_data*/) => {
|
.then((/*access_data*/) => {
|
||||||
// TODO: at this point the existing streams should have been checked
|
// TODO: at this point the existing streams should have been checked
|
||||||
return internalStream.get(access, { id: thisData.id });
|
return internalStream.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (row.id !== thisData.id) {
|
if (row.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
|
||||||
`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (create_certificate) {
|
|
||||||
return internalCertificate
|
|
||||||
.createQuickCertificate(access, {
|
|
||||||
domain_names: thisData.domain_names || row.domain_names,
|
|
||||||
meta: _.assign({}, row.meta, thisData.meta),
|
|
||||||
})
|
|
||||||
.then((cert) => {
|
|
||||||
// update host with cert id
|
|
||||||
thisData.certificate_id = cert.id;
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
thisData = _.assign(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
domain_names: row.domain_names,
|
|
||||||
},
|
|
||||||
thisData,
|
|
||||||
);
|
|
||||||
|
|
||||||
return streamModel
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.patchAndFetchById(row.id, thisData)
|
.omit(omissions())
|
||||||
.then(utils.omitRow(omissions()))
|
.patchAndFetchById(row.id, data)
|
||||||
|
.then((saved_row) => {
|
||||||
|
return internalNginx.configure(streamModel, 'stream', saved_row)
|
||||||
|
.then(() => {
|
||||||
|
return internalStream.get(access, {id: row.id, expand: ['owner']});
|
||||||
|
});
|
||||||
|
})
|
||||||
.then((saved_row) => {
|
.then((saved_row) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'updated',
|
||||||
action: "updated",
|
object_type: 'stream',
|
||||||
object_type: "stream",
|
object_id: row.id,
|
||||||
object_id: row.id,
|
meta: data
|
||||||
meta: thisData,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return saved_row;
|
return _.omit(saved_row, omissions());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return internalStream.get(access, { id: thisData.id, expand: ["owner", "certificate"] }).then((row) => {
|
|
||||||
return internalNginx.configure(streamModel, "stream", row).then((new_meta) => {
|
|
||||||
row.meta = new_meta;
|
|
||||||
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -177,38 +103,40 @@ const internalStream = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: (access, data) => {
|
get: (access, data) => {
|
||||||
const thisData = data || {};
|
if (typeof data === 'undefined') {
|
||||||
return access
|
data = {};
|
||||||
.can("streams:get", thisData.id)
|
}
|
||||||
|
|
||||||
|
return access.can('streams:get', data.id)
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const query = streamModel
|
let query = streamModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere("id", thisData.id)
|
.andWhere('id', data.id)
|
||||||
.allowGraph(streamModel.defaultAllowGraph)
|
.allowEager('[owner]')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (access_data.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
// Custom omissions
|
||||||
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRow(omissions()));
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
let thisRow = row;
|
if (row) {
|
||||||
if (!thisRow?.id) {
|
return _.omit(row, omissions());
|
||||||
throw new errs.ItemNotFoundError(thisData.id);
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
thisRow = internalHost.cleanRowCertificateMeta(thisRow);
|
|
||||||
// Custom omissions
|
|
||||||
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
|
|
||||||
return _.omit(thisRow, thisData.omit);
|
|
||||||
}
|
|
||||||
return thisRow;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -220,35 +148,35 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: (access, data) => {
|
delete: (access, data) => {
|
||||||
return access
|
return access.can('streams:delete', data.id)
|
||||||
.can("streams:delete", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalStream.get(access, { id: data.id });
|
return internalStream.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return streamModel
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
is_deleted: 1,
|
is_deleted: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("stream", row).then(() => {
|
return internalNginx.deleteConfig('stream', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "deleted",
|
action: 'deleted',
|
||||||
object_type: "stream",
|
object_type: 'stream',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -265,41 +193,39 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
enable: (access, data) => {
|
enable: (access, data) => {
|
||||||
return access
|
return access.can('streams:update', data.id)
|
||||||
.can("streams:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalStream.get(access, {
|
return internalStream.get(access, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
expand: ["certificate", "owner"],
|
expand: ['owner']
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (row.enabled) {
|
||||||
if (row.enabled) {
|
throw new error.ValidationError('Host is already enabled');
|
||||||
throw new errs.ValidationError("Stream is already enabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 1;
|
row.enabled = 1;
|
||||||
|
|
||||||
return streamModel
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
enabled: 1,
|
enabled: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Configure nginx
|
// Configure nginx
|
||||||
return internalNginx.configure(streamModel, "stream", row);
|
return internalNginx.configure(streamModel, 'stream', row);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "enabled",
|
action: 'enabled',
|
||||||
object_type: "stream",
|
object_type: 'stream',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -316,40 +242,39 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
disable: (access, data) => {
|
disable: (access, data) => {
|
||||||
return access
|
return access.can('streams:update', data.id)
|
||||||
.can("streams:update", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalStream.get(access, { id: data.id });
|
return internalStream.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (!row) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
} else if (!row.enabled) {
|
||||||
if (!row.enabled) {
|
throw new error.ValidationError('Host is already disabled');
|
||||||
throw new errs.ValidationError("Stream is already disabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
row.enabled = 0;
|
row.enabled = 0;
|
||||||
|
|
||||||
return streamModel
|
return streamModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", row.id)
|
.where('id', row.id)
|
||||||
.patch({
|
.patch({
|
||||||
enabled: 0,
|
enabled: 0
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Delete Nginx Config
|
// Delete Nginx Config
|
||||||
return internalNginx.deleteConfig("stream", row).then(() => {
|
return internalNginx.deleteConfig('stream', row)
|
||||||
return internalNginx.reload();
|
.then(() => {
|
||||||
});
|
return internalNginx.reload();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "disabled",
|
action: 'disabled',
|
||||||
object_type: "stream",
|
object_type: 'stream-host',
|
||||||
object_id: row.id,
|
object_id: row.id,
|
||||||
meta: _.omit(row, omissions()),
|
meta: _.omit(row, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -367,39 +292,32 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: (access, expand, search_query) => {
|
getAll: (access, expand, search_query) => {
|
||||||
return access
|
return access.can('streams:list')
|
||||||
.can("streams:list")
|
|
||||||
.then((access_data) => {
|
.then((access_data) => {
|
||||||
const query = streamModel
|
let query = streamModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.groupBy("id")
|
.groupBy('id')
|
||||||
.allowGraph(streamModel.defaultAllowGraph)
|
.omit(['is_deleted'])
|
||||||
.orderBy("incoming_port", "ASC");
|
.allowEager('[owner]')
|
||||||
|
.orderBy('incoming_port', 'ASC');
|
||||||
|
|
||||||
if (access_data.permission_visibility !== "all") {
|
if (access_data.permission_visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", access.token.getUserId(1));
|
query.andWhere('owner_user_id', access.token.getUserId(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query is used for searching
|
// Query is used for searching
|
||||||
if (typeof search_query === "string" && search_query.length > 0) {
|
if (typeof search_query === 'string') {
|
||||||
query.where(function () {
|
query.where(function () {
|
||||||
this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`);
|
this.where('incoming_port', 'like', '%' + search_query + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRows(omissions()));
|
return query;
|
||||||
})
|
|
||||||
.then((rows) => {
|
|
||||||
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
|
|
||||||
return internalHost.cleanAllRowsCertificateMeta(rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -411,16 +329,20 @@ const internalStream = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getCount: (user_id, visibility) => {
|
getCount: (user_id, visibility) => {
|
||||||
const query = streamModel.query().count("id AS count").where("is_deleted", 0);
|
let query = streamModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0);
|
||||||
|
|
||||||
if (visibility !== "all") {
|
if (visibility !== 'all') {
|
||||||
query.andWhere("owner_user_id", user_id);
|
query.andWhere('owner_user_id', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.first().then((row) => {
|
return query.first()
|
||||||
return Number.parseInt(row.count, 10);
|
.then((row) => {
|
||||||
});
|
return parseInt(row.count, 10);
|
||||||
},
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalStream;
|
module.exports = internalStream;
|
||||||
+110
-177
@@ -1,17 +1,12 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
import errs from "../lib/error.js";
|
const error = require('../lib/error');
|
||||||
import { parseDatePeriod } from "../lib/helpers.js";
|
const userModel = require('../models/user');
|
||||||
import authModel from "../models/auth.js";
|
const authModel = require('../models/auth');
|
||||||
import TokenModel from "../models/token.js";
|
const helpers = require('../lib/helpers');
|
||||||
import userModel from "../models/user.js";
|
const TokenModel = require('../models/token');
|
||||||
import twoFactor from "./2fa.js";
|
|
||||||
|
|
||||||
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
|
module.exports = {
|
||||||
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
|
|
||||||
const ERROR_MESSAGE_INVALID_2FA = "Invalid verification code";
|
|
||||||
const ERROR_MESSAGE_INVALID_2FA_I18N = "error.invalid-2fa";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @param {String} data.identity
|
* @param {String} data.identity
|
||||||
@@ -21,80 +16,70 @@ export default {
|
|||||||
* @param {String} [issuer]
|
* @param {String} [issuer]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getTokenFromEmail: async (data, issuer) => {
|
getTokenFromEmail: (data, issuer) => {
|
||||||
const Token = TokenModel();
|
let Token = new TokenModel();
|
||||||
|
|
||||||
data.scope = data.scope || "user";
|
data.scope = data.scope || 'user';
|
||||||
data.expiry = data.expiry || "1d";
|
data.expiry = data.expiry || '1d';
|
||||||
|
|
||||||
const user = await userModel
|
return userModel
|
||||||
.query()
|
.query()
|
||||||
.where("email", data.identity.toLowerCase().trim())
|
.where('email', data.identity)
|
||||||
.andWhere("is_deleted", 0)
|
.andWhere('is_deleted', 0)
|
||||||
.andWhere("is_disabled", 0)
|
.andWhere('is_disabled', 0)
|
||||||
.first();
|
.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) {
|
||||||
|
|
||||||
// An unknown, disabled or passwordless account goes through the same queries and the same password check
|
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
|
||||||
// as a real one, and fails with the same error: answering any sooner or any differently would tell anyone
|
// The scope requested doesn't exist as a role against the user,
|
||||||
// trying addresses which ones have an account. There is no user 0, so its lookup finds nothing. An empty
|
// you shall not pass.
|
||||||
// secret, which creating a user through the API can store, is no hash: bcrypt would reject it at once.
|
throw new error.AuthError('Invalid scope: ' + data.scope);
|
||||||
const auth = await authModel
|
}
|
||||||
.query()
|
|
||||||
.where("user_id", "=", user?.id ?? 0)
|
|
||||||
.where("type", "=", "password")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
const valid = auth?.secret
|
// Create a moment of the expiry expression
|
||||||
? await auth.verifyPassword(data.secret)
|
let expiry = helpers.parseDatePeriod(data.expiry);
|
||||||
: await authModel.verifyDummyPassword(data.secret);
|
if (expiry === null) {
|
||||||
if (!user || !valid) {
|
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
||||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH, ERROR_MESSAGE_INVALID_AUTH_I18N);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
|
return Token.create({
|
||||||
// The scope requested doesn't exist as a role against the user,
|
iss: issuer || 'api',
|
||||||
// you shall not pass.
|
attrs: {
|
||||||
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
|
id: user.id
|
||||||
}
|
},
|
||||||
|
scope: [data.scope],
|
||||||
// Check if 2FA is enabled
|
expiresIn: data.expiry
|
||||||
const has2FA = await twoFactor.isEnabled(user.id);
|
})
|
||||||
if (has2FA) {
|
.then((signed) => {
|
||||||
// Return challenge token instead of full token
|
return {
|
||||||
const challengeToken = await Token.create({
|
token: signed.token,
|
||||||
iss: issuer || "api",
|
expires: expiry.toISOString()
|
||||||
attrs: {
|
};
|
||||||
id: user.id,
|
});
|
||||||
},
|
} else {
|
||||||
scope: ["2fa-challenge"],
|
throw new error.AuthError('Invalid password');
|
||||||
expiresIn: "5m",
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new error.AuthError('No password auth for user');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new error.AuthError('No relevant user found');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
requires_2fa: true,
|
|
||||||
challenge_token: challengeToken.token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a moment of the expiry expression
|
|
||||||
const expiry = parseDatePeriod(data.expiry);
|
|
||||||
if (expiry === null) {
|
|
||||||
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const signed = await Token.create({
|
|
||||||
iss: issuer || "api",
|
|
||||||
attrs: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
scope: [data.scope],
|
|
||||||
expiresIn: data.expiry,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: signed.token,
|
|
||||||
expires: expiry.toISOString(),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,126 +89,74 @@ export default {
|
|||||||
* @param {String} [data.scope] Only considered if existing token scope is admin
|
* @param {String} [data.scope] Only considered if existing token scope is admin
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getFreshToken: async (access, data) => {
|
getFreshToken: (access, data) => {
|
||||||
const Token = TokenModel();
|
let Token = new TokenModel();
|
||||||
const thisData = data || {};
|
|
||||||
|
|
||||||
thisData.expiry = thisData.expiry || "1d";
|
data = data || {};
|
||||||
|
data.expiry = data.expiry || '1d';
|
||||||
|
|
||||||
|
if (access && access.token.getUserId(0)) {
|
||||||
|
|
||||||
if (access?.token.getUserId(0)) {
|
|
||||||
// Create a moment of the expiry expression
|
// Create a moment of the expiry expression
|
||||||
const expiry = parseDatePeriod(thisData.expiry);
|
let expiry = helpers.parseDatePeriod(data.expiry);
|
||||||
if (expiry === null) {
|
if (expiry === null) {
|
||||||
throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);
|
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token_attrs = {
|
let token_attrs = {
|
||||||
id: access.token.getUserId(0),
|
id: access.token.getUserId(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only admins can request otherwise scoped tokens
|
// Only admins can request otherwise scoped tokens
|
||||||
let scope = access.token.get("scope");
|
let scope = access.token.get('scope');
|
||||||
if (thisData.scope && access.token.hasScope("admin")) {
|
if (data.scope && access.token.hasScope('admin')) {
|
||||||
scope = [thisData.scope];
|
scope = [data.scope];
|
||||||
|
|
||||||
if (thisData.scope === "job-board" || thisData.scope === "worker") {
|
if (data.scope === 'job-board' || data.scope === 'worker') {
|
||||||
token_attrs.id = 0;
|
token_attrs.id = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signed = await Token.create({
|
return Token.create({
|
||||||
iss: "api",
|
iss: 'api',
|
||||||
scope: scope,
|
scope: scope,
|
||||||
attrs: token_attrs,
|
attrs: token_attrs,
|
||||||
expiresIn: thisData.expiry,
|
expiresIn: data.expiry
|
||||||
});
|
})
|
||||||
|
.then((signed) => {
|
||||||
return {
|
return {
|
||||||
token: signed.token,
|
token: signed.token,
|
||||||
expires: expiry.toISOString(),
|
expires: expiry.toISOString()
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new error.AssertionFailedError('Existing token contained invalid user data');
|
||||||
}
|
}
|
||||||
throw new error.AssertionFailedError("Existing token contained invalid user data");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify 2FA code and return full token
|
|
||||||
* @param {string} challengeToken
|
|
||||||
* @param {string} code
|
|
||||||
* @param {string} [expiry]
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
verify2FA: async (challengeToken, code, expiry) => {
|
|
||||||
const Token = TokenModel();
|
|
||||||
const tokenExpiry = expiry || "1d";
|
|
||||||
|
|
||||||
// Verify challenge token
|
|
||||||
let tokenData;
|
|
||||||
try {
|
|
||||||
tokenData = await Token.load(challengeToken);
|
|
||||||
} catch {
|
|
||||||
throw new errs.AuthError("Invalid or expired challenge token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check scope
|
|
||||||
if (tokenData.scope?.[0] !== "2fa-challenge") {
|
|
||||||
throw new errs.AuthError("Invalid challenge token");
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = tokenData.attrs?.id;
|
|
||||||
if (!userId) {
|
|
||||||
throw new errs.AuthError("Invalid challenge token");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 2FA code
|
|
||||||
const valid = await twoFactor.verifyForLogin(userId, code);
|
|
||||||
if (!valid) {
|
|
||||||
throw new errs.AuthError(ERROR_MESSAGE_INVALID_2FA, ERROR_MESSAGE_INVALID_2FA_I18N);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create full token
|
|
||||||
const expiryDate = parseDatePeriod(tokenExpiry);
|
|
||||||
if (expiryDate === null) {
|
|
||||||
throw new errs.AuthError(`Invalid expiry time: ${tokenExpiry}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const signed = await Token.create({
|
|
||||||
iss: "api",
|
|
||||||
attrs: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
scope: ["user"],
|
|
||||||
expiresIn: tokenExpiry,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: signed.token,
|
|
||||||
expires: expiryDate.toISOString(),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} user
|
* @param {Object} user
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getTokenFromUser: async (user) => {
|
getTokenFromUser: (user) => {
|
||||||
const expire = "1d";
|
const expire = '1d';
|
||||||
const Token = TokenModel();
|
const Token = new TokenModel();
|
||||||
const expiry = parseDatePeriod(expire);
|
const expiry = helpers.parseDatePeriod(expire);
|
||||||
|
|
||||||
const signed = await Token.create({
|
return Token.create({
|
||||||
iss: "api",
|
iss: 'api',
|
||||||
attrs: {
|
attrs: {
|
||||||
id: user.id,
|
id: user.id
|
||||||
},
|
},
|
||||||
scope: ["user"],
|
scope: ['user'],
|
||||||
expiresIn: expire,
|
expiresIn: expire
|
||||||
});
|
})
|
||||||
|
.then((signed) => {
|
||||||
return {
|
return {
|
||||||
token: signed.token,
|
token: signed.token,
|
||||||
expires: expiry.toISOString(),
|
expires: expiry.toISOString(),
|
||||||
user: user,
|
user: user
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
+253
-275
@@ -1,77 +1,93 @@
|
|||||||
import gravatar from "gravatar";
|
const _ = require('lodash');
|
||||||
import _ from "lodash";
|
const error = require('../lib/error');
|
||||||
import errs from "../lib/error.js";
|
const userModel = require('../models/user');
|
||||||
import utils from "../lib/utils.js";
|
const userPermissionModel = require('../models/user_permission');
|
||||||
import authModel from "../models/auth.js";
|
const authModel = require('../models/auth');
|
||||||
import proxyHostModel from "../models/proxy_host.js";
|
const gravatar = require('gravatar');
|
||||||
import userModel from "../models/user.js";
|
const internalToken = require('./token');
|
||||||
import userPermissionModel from "../models/user_permission.js";
|
const internalAuditLog = require('./audit-log');
|
||||||
import internalAuditLog from "./audit-log.js";
|
|
||||||
import internalToken from "./token.js";
|
|
||||||
|
|
||||||
const omissions = () => {
|
function omissions () {
|
||||||
return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"];
|
return ['is_deleted'];
|
||||||
};
|
}
|
||||||
|
|
||||||
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" });
|
|
||||||
|
|
||||||
const internalUser = {
|
const internalUser = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a user can happen unauthenticated only once and only when no active users exist.
|
|
||||||
* Otherwise, a valid auth method is required.
|
|
||||||
*
|
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
create: async (access, data) => {
|
create: (access, data) => {
|
||||||
const auth = data.auth || null;
|
let auth = data.auth || null;
|
||||||
delete data.auth;
|
delete data.auth;
|
||||||
|
|
||||||
data.avatar = data.avatar || "";
|
data.avatar = data.avatar || '';
|
||||||
data.roles = data.roles || [];
|
data.roles = data.roles || [];
|
||||||
|
|
||||||
if (typeof data.is_disabled !== "undefined") {
|
if (typeof data.is_disabled !== 'undefined') {
|
||||||
data.is_disabled = data.is_disabled ? 1 : 0;
|
data.is_disabled = data.is_disabled ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await access.can("users:create", data);
|
return access.can('users:create', data)
|
||||||
data.avatar = gravatar.url(data.email, { default: "mm" });
|
.then(() => {
|
||||||
|
data.avatar = gravatar.url(data.email, {default: 'mm'});
|
||||||
|
|
||||||
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
|
return userModel
|
||||||
if (auth) {
|
.query()
|
||||||
user = await authModel.query().insert({
|
.omit(omissions())
|
||||||
user_id: user.id,
|
.insertAndFetch(data);
|
||||||
type: auth.type,
|
})
|
||||||
secret: auth.secret,
|
.then((user) => {
|
||||||
meta: {},
|
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',
|
||||||
|
ssl_passthrough_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;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Create permissions row as well
|
|
||||||
const isAdmin = data.roles.indexOf("admin") !== -1;
|
|
||||||
|
|
||||||
await userPermissionModel.query().insert({
|
|
||||||
user_id: user.id,
|
|
||||||
visibility: isAdmin ? "all" : "user",
|
|
||||||
proxy_hosts: "manage",
|
|
||||||
redirection_hosts: "manage",
|
|
||||||
dead_hosts: "manage",
|
|
||||||
streams: "manage",
|
|
||||||
access_lists: "manage",
|
|
||||||
certificates: "manage",
|
|
||||||
});
|
|
||||||
|
|
||||||
user = await internalUser.get(access, { id: user.id, expand: ["permissions"] });
|
|
||||||
|
|
||||||
await internalAuditLog.add(access, {
|
|
||||||
action: "created",
|
|
||||||
object_type: "user",
|
|
||||||
object_id: user.id,
|
|
||||||
meta: user,
|
|
||||||
});
|
|
||||||
|
|
||||||
return user;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,63 +99,65 @@ const internalUser = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
update: (access, data) => {
|
update: (access, data) => {
|
||||||
if (typeof data.is_disabled !== "undefined") {
|
if (typeof data.is_disabled !== 'undefined') {
|
||||||
data.is_disabled = data.is_disabled ? 1 : 0;
|
data.is_disabled = data.is_disabled ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
return access.can('users:update', data.id)
|
||||||
.can("users:permissions", data.id)
|
|
||||||
.catch(() => {
|
|
||||||
delete data.roles;
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
return access.can("users:update", data.id);
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
// Make sure that the user being updated doesn't change their email to another user that is already using it
|
// 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
|
// 1. get user we want to update
|
||||||
return internalUser.get(access, { id: data.id }).then((user) => {
|
return internalUser.get(access, {id: data.id})
|
||||||
// 2. if email is to be changed, find other users with that email
|
.then((user) => {
|
||||||
if (typeof data.email !== "undefined") {
|
|
||||||
data.email = data.email.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (user.email !== data.email) {
|
// 2. if email is to be changed, find other users with that email
|
||||||
return internalUser.isEmailAvailable(data.email, data.id).then((available) => {
|
if (typeof data.email !== 'undefined') {
|
||||||
if (!available) {
|
data.email = data.email.toLowerCase().trim();
|
||||||
throw new errs.ValidationError(`Email address already in use - ${data.email}`);
|
|
||||||
}
|
if (user.email !== data.email) {
|
||||||
return user;
|
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:
|
// No change to email:
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (user.id !== data.id) {
|
if (user.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
|
||||||
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.avatar = gravatar.url(data.email || user.email, { default: "mm" });
|
data.avatar = gravatar.url(data.email || user.email, {default: 'mm'});
|
||||||
return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));
|
|
||||||
|
return userModel
|
||||||
|
.query()
|
||||||
|
.omit(omissions())
|
||||||
|
.patchAndFetchById(user.id, data)
|
||||||
|
.then((saved_user) => {
|
||||||
|
return _.omit(saved_user, omissions());
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalUser.get(access, { id: data.id });
|
return internalUser.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog
|
return internalAuditLog.add(access, {
|
||||||
.add(access, {
|
action: 'updated',
|
||||||
action: "updated",
|
object_type: 'user',
|
||||||
object_type: "user",
|
object_id: user.id,
|
||||||
object_id: user.id,
|
meta: data
|
||||||
meta: { ...data, id: user.id, name: user.name },
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
@@ -155,42 +173,40 @@ const internalUser = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
get: (access, data) => {
|
get: (access, data) => {
|
||||||
const thisData = data || {};
|
if (typeof data === 'undefined') {
|
||||||
|
data = {};
|
||||||
if (typeof thisData.id === "undefined" || !thisData.id) {
|
|
||||||
thisData.id = access.token.getUserId(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return access
|
if (typeof data.id === 'undefined' || !data.id) {
|
||||||
.can("users:get", thisData.id)
|
data.id = access.token.getUserId(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return access.can('users:get', data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const query = userModel
|
let query = userModel
|
||||||
.query()
|
.query()
|
||||||
.where("is_deleted", 0)
|
.where('is_deleted', 0)
|
||||||
.andWhere("id", thisData.id)
|
.andWhere('id', data.id)
|
||||||
.allowGraph("[permissions]")
|
.allowEager('[permissions]')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
|
// Custom omissions
|
||||||
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
|
if (typeof data.omit !== 'undefined' && data.omit !== null) {
|
||||||
|
query.omit(data.omit);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then(utils.omitRow(omissions()));
|
if (typeof data.expand !== 'undefined' && data.expand !== null) {
|
||||||
|
query.eager('[' + data.expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
if (!row?.id) {
|
if (row) {
|
||||||
throw new errs.ItemNotFoundError(thisData.id);
|
return _.omit(row, omissions());
|
||||||
|
} else {
|
||||||
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
// Custom omissions
|
|
||||||
if (typeof thisData.omit !== "undefined" && thisData.omit !== null) {
|
|
||||||
return _.omit(row, thisData.omit);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.avatar === "") {
|
|
||||||
row.avatar = DEFAULT_AVATAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
return row;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -202,15 +218,20 @@ const internalUser = {
|
|||||||
* @param user_id
|
* @param user_id
|
||||||
*/
|
*/
|
||||||
isEmailAvailable: (email, user_id) => {
|
isEmailAvailable: (email, user_id) => {
|
||||||
const query = userModel.query().where("email", "=", email.toLowerCase().trim()).where("is_deleted", 0).first();
|
let query = userModel
|
||||||
|
.query()
|
||||||
|
.where('email', '=', email.toLowerCase().trim())
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.first();
|
||||||
|
|
||||||
if (typeof user_id !== "undefined") {
|
if (typeof user_id !== 'undefined') {
|
||||||
query.where("id", "!=", user_id);
|
query.where('id', '!=', user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return query.then((user) => {
|
return query
|
||||||
return !user;
|
.then((user) => {
|
||||||
});
|
return !user;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,34 +242,33 @@ const internalUser = {
|
|||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
delete: (access, data) => {
|
delete: (access, data) => {
|
||||||
return access
|
return access.can('users:delete', data.id)
|
||||||
.can("users:delete", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalUser.get(access, { id: data.id });
|
return internalUser.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new errs.ItemNotFoundError(data.id);
|
throw new error.ItemNotFoundError(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure user can't delete themselves
|
// Make sure user can't delete themselves
|
||||||
if (user.id === access.token.getUserId(0)) {
|
if (user.id === access.token.getUserId(0)) {
|
||||||
throw new errs.PermissionError("You cannot delete yourself.");
|
throw new error.PermissionError('You cannot delete yourself.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return userModel
|
return userModel
|
||||||
.query()
|
.query()
|
||||||
.where("id", user.id)
|
.where('id', user.id)
|
||||||
.patch({
|
.patch({
|
||||||
is_deleted: 1,
|
is_deleted: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to audit log
|
// Add to audit log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "deleted",
|
action: 'deleted',
|
||||||
object_type: "user",
|
object_type: 'user',
|
||||||
object_id: user.id,
|
object_id: user.id,
|
||||||
meta: _.omit(user, omissions()),
|
meta: _.omit(user, omissions())
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -257,12 +277,6 @@ const internalUser = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteAll: async () => {
|
|
||||||
await userModel.query().patch({
|
|
||||||
is_deleted: 1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will only count the users
|
* This will only count the users
|
||||||
*
|
*
|
||||||
@@ -271,26 +285,26 @@ const internalUser = {
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getCount: (access, search_query) => {
|
getCount: (access, search_query) => {
|
||||||
return access
|
return access.can('users:list')
|
||||||
.can("users:list")
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const query = userModel.query().count("id as count").where("is_deleted", 0).first();
|
let query = userModel
|
||||||
|
.query()
|
||||||
|
.count('id as count')
|
||||||
|
.where('is_deleted', 0)
|
||||||
|
.first();
|
||||||
|
|
||||||
// Query is used for searching
|
// Query is used for searching
|
||||||
if (typeof search_query === "string") {
|
if (typeof search_query === 'string') {
|
||||||
query.where(function () {
|
query.where(function () {
|
||||||
this.where("user.name", "like", `%${search_query}%`).orWhere(
|
this.where('user.name', 'like', '%' + search_query + '%')
|
||||||
"user.email",
|
.orWhere('user.email', 'like', '%' + search_query + '%');
|
||||||
"like",
|
|
||||||
`%${search_query}%`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
})
|
})
|
||||||
.then((row) => {
|
.then((row) => {
|
||||||
return Number.parseInt(row.count, 10);
|
return parseInt(row.count, 10);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -302,28 +316,31 @@ const internalUser = {
|
|||||||
* @param {String} [search_query]
|
* @param {String} [search_query]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
getAll: async (access, expand, search_query) => {
|
getAll: (access, expand, search_query) => {
|
||||||
await access.can("users:list");
|
return access.can('users:list')
|
||||||
const query = userModel
|
.then(() => {
|
||||||
.query()
|
let query = userModel
|
||||||
.where("is_deleted", 0)
|
.query()
|
||||||
.groupBy("id")
|
.where('is_deleted', 0)
|
||||||
.allowGraph("[permissions]")
|
.groupBy('id')
|
||||||
.orderBy("name", "ASC");
|
.omit(['is_deleted'])
|
||||||
|
.allowEager('[permissions]')
|
||||||
|
.orderBy('name', 'ASC');
|
||||||
|
|
||||||
// Query is used for searching
|
// Query is used for searching
|
||||||
if (typeof search_query === "string") {
|
if (typeof search_query === 'string') {
|
||||||
query.where(function () {
|
query.where(function () {
|
||||||
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`);
|
this.where('name', 'like', '%' + search_query + '%')
|
||||||
|
.orWhere('email', 'like', '%' + search_query + '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expand !== 'undefined' && expand !== null) {
|
||||||
|
query.eager('[' + expand.join(', ') + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof expand !== "undefined" && expand !== null) {
|
|
||||||
query.withGraphFetched(`[${expand.join(", ")}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await query;
|
|
||||||
return utils.omitRows(omissions())(res);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -331,11 +348,11 @@ const internalUser = {
|
|||||||
* @param {Integer} [id_requested]
|
* @param {Integer} [id_requested]
|
||||||
* @returns {[String]}
|
* @returns {[String]}
|
||||||
*/
|
*/
|
||||||
getUserOmisionsByAccess: (access, idRequested) => {
|
getUserOmisionsByAccess: (access, id_requested) => {
|
||||||
let response = []; // Admin response
|
let response = []; // Admin response
|
||||||
|
|
||||||
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
|
if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) {
|
||||||
response = ["is_deleted"]; // Restricted response
|
response = ['roles', 'is_deleted']; // Restricted response
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -350,30 +367,26 @@ const internalUser = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
setPassword: (access, data) => {
|
setPassword: (access, data) => {
|
||||||
return access
|
return access.can('users:password', data.id)
|
||||||
.can("users:password", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalUser.get(access, { id: data.id });
|
return internalUser.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (user.id !== data.id) {
|
if (user.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
|
||||||
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.id === access.token.getUserId(0)) {
|
if (user.id === access.token.getUserId(0)) {
|
||||||
// they're setting their own password. Make sure their current password is correct
|
// they're setting their own password. Make sure their current password is correct
|
||||||
if (typeof data.current === "undefined" || !data.current) {
|
if (typeof data.current === 'undefined' || !data.current) {
|
||||||
throw new errs.ValidationError("Current password was not supplied");
|
throw new error.ValidationError('Current password was not supplied');
|
||||||
}
|
}
|
||||||
|
|
||||||
return internalToken
|
return internalToken.getTokenFromEmail({
|
||||||
.getTokenFromEmail({
|
identity: user.email,
|
||||||
identity: user.email,
|
secret: data.current
|
||||||
secret: data.current,
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return user;
|
return user;
|
||||||
});
|
});
|
||||||
@@ -385,46 +398,43 @@ const internalUser = {
|
|||||||
// Get auth, patch if it exists
|
// Get auth, patch if it exists
|
||||||
return authModel
|
return authModel
|
||||||
.query()
|
.query()
|
||||||
.where("user_id", user.id)
|
.where('user_id', user.id)
|
||||||
.andWhere("type", data.type)
|
.andWhere('type', data.type)
|
||||||
.first()
|
.first()
|
||||||
.then((existing_auth) => {
|
.then((existing_auth) => {
|
||||||
// Stamped here rather than read off modified_on, because it is compared against a
|
|
||||||
// token's `iat` and the two only line up when the same clock writes both. The
|
|
||||||
// database clock is a different one: with the app on one timezone and the database
|
|
||||||
// on another, its timestamps come back hours away from where Node thinks it is.
|
|
||||||
const password_changed_at = Math.floor(Date.now() / 1000);
|
|
||||||
|
|
||||||
if (existing_auth) {
|
if (existing_auth) {
|
||||||
// patch
|
// patch
|
||||||
const meta = existing_auth.meta || {};
|
return authModel
|
||||||
meta.password_changed_at = password_changed_at;
|
.query()
|
||||||
|
.where('user_id', user.id)
|
||||||
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({
|
.andWhere('type', data.type)
|
||||||
type: data.type, // This is required for the model to encrypt on save
|
.patch({
|
||||||
secret: data.secret,
|
type: data.type, // This is required for the model to encrypt on save
|
||||||
meta,
|
secret: data.secret
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// insert
|
||||||
|
return authModel
|
||||||
|
.query()
|
||||||
|
.insert({
|
||||||
|
user_id: user.id,
|
||||||
|
type: data.type,
|
||||||
|
secret: data.secret,
|
||||||
|
meta: {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// insert
|
|
||||||
return authModel.query().insert({
|
|
||||||
user_id: user.id,
|
|
||||||
type: data.type,
|
|
||||||
secret: data.secret,
|
|
||||||
meta: { password_changed_at },
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add to Audit Log
|
// Add to Audit Log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "updated",
|
action: 'updated',
|
||||||
object_type: "user",
|
object_type: 'user',
|
||||||
object_id: user.id,
|
object_id: user.id,
|
||||||
meta: {
|
meta: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
password_changed: true,
|
password_changed: true,
|
||||||
auth_type: data.type,
|
auth_type: data.type
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -439,51 +449,50 @@ const internalUser = {
|
|||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
setPermissions: (access, data) => {
|
setPermissions: (access, data) => {
|
||||||
return access
|
return access.can('users:permissions', data.id)
|
||||||
.can("users:permissions", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalUser.get(access, { id: data.id });
|
return internalUser.get(access, {id: data.id});
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (Number(user.id) !== Number(data.id)) {
|
if (user.id !== data.id) {
|
||||||
// Sanity check that something crazy hasn't happened
|
// Sanity check that something crazy hasn't happened
|
||||||
throw new errs.InternalValidationError(
|
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
|
||||||
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
const permData = _.omit(data, ["id"]);
|
|
||||||
// Get perms row, patch if it exists
|
// Get perms row, patch if it exists
|
||||||
return userPermissionModel
|
return userPermissionModel
|
||||||
.query()
|
.query()
|
||||||
.where("user_id", user.id)
|
.where('user_id', user.id)
|
||||||
.first()
|
.first()
|
||||||
.then(async (existing_auth) => {
|
.then((existing_auth) => {
|
||||||
permData.meta = await internalUser.normalisePermissionMeta(existing_auth, permData);
|
|
||||||
if (existing_auth) {
|
if (existing_auth) {
|
||||||
// patch
|
// patch
|
||||||
return userPermissionModel
|
return userPermissionModel
|
||||||
.query()
|
.query()
|
||||||
.where("user_id", user.id)
|
.where('user_id', user.id)
|
||||||
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, permData));
|
.patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data));
|
||||||
|
} else {
|
||||||
|
// insert
|
||||||
|
return userPermissionModel
|
||||||
|
.query()
|
||||||
|
.insertAndFetch(_.assign({user_id: user.id}, data));
|
||||||
}
|
}
|
||||||
// insert
|
|
||||||
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, permData));
|
|
||||||
})
|
})
|
||||||
.then((permissions) => {
|
.then((permissions) => {
|
||||||
// Add to Audit Log
|
// Add to Audit Log
|
||||||
return internalAuditLog.add(access, {
|
return internalAuditLog.add(access, {
|
||||||
action: "updated",
|
action: 'updated',
|
||||||
object_type: "user",
|
object_type: 'user',
|
||||||
object_id: user.id,
|
object_id: user.id,
|
||||||
meta: {
|
meta: {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
permissions: permissions,
|
permissions: permissions
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -491,51 +500,20 @@ const internalUser = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* The permission meta to store. Proxy host grants (meta.proxy_host_ids) only apply while
|
|
||||||
* visibility is "user" and proxy hosts are not hidden, so they're cleared otherwise; kept,
|
|
||||||
* they would silently come back when proxy hosts are shown again. Ids of hosts that no
|
|
||||||
* longer exist are dropped. `permData` may be partial, so the stored values fill the gaps.
|
|
||||||
*
|
|
||||||
* @param {Object} [existing] the stored user_permission row
|
|
||||||
* @param {Object} permData incoming permission fields
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
|
||||||
normalisePermissionMeta: async (existing, permData) => {
|
|
||||||
const effective = _.assign(
|
|
||||||
{ visibility: "user", proxy_hosts: "hidden" },
|
|
||||||
existing || {},
|
|
||||||
_.omit(permData, ["meta"]),
|
|
||||||
);
|
|
||||||
const meta = _.assign({}, existing?.meta || {}, permData.meta || {});
|
|
||||||
const requested = Array.isArray(meta.proxy_host_ids) ? _.uniq(meta.proxy_host_ids) : [];
|
|
||||||
|
|
||||||
if (effective.visibility !== "user" || effective.proxy_hosts === "hidden" || !requested.length) {
|
|
||||||
meta.proxy_host_ids = [];
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await proxyHostModel.query().select("id").where("is_deleted", 0).whereIn("id", requested);
|
|
||||||
const found = new Set(rows.map((r) => r.id));
|
|
||||||
meta.proxy_host_ids = requested.filter((id) => found.has(id));
|
|
||||||
return meta;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Access} access
|
* @param {Access} access
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @param {Integer} data.id
|
* @param {Integer} data.id
|
||||||
*/
|
*/
|
||||||
loginAs: (access, data) => {
|
loginAs: (access, data) => {
|
||||||
return access
|
return access.can('users:loginas', data.id)
|
||||||
.can("users:loginas", data.id)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return internalUser.get(access, data);
|
return internalUser.get(access, data);
|
||||||
})
|
})
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
return internalToken.getTokenFromUser(user);
|
return internalToken.getTokenFromUser(user);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default internalUser;
|
module.exports = internalUser;
|
||||||
+11
-11
@@ -1,19 +1,19 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
development: {
|
development: {
|
||||||
client: "mysql2",
|
client: 'mysql',
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "migrations",
|
tableName: 'migrations',
|
||||||
stub: "lib/migrate_template.js",
|
stub: 'lib/migrate_template.js',
|
||||||
directory: "migrations",
|
directory: 'migrations'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
production: {
|
production: {
|
||||||
client: "mysql2",
|
client: 'mysql',
|
||||||
migrations: {
|
migrations: {
|
||||||
tableName: "migrations",
|
tableName: 'migrations',
|
||||||
stub: "lib/migrate_template.js",
|
stub: 'lib/migrate_template.js',
|
||||||
directory: "migrations",
|
directory: 'migrations'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
+241
-231
@@ -4,107 +4,91 @@
|
|||||||
* "scope" in this file means "where did this token come from and what is using it", so 99% of the time
|
* "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 "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.
|
* the "role" which could be "user" or "admin". The scope in fact, could be "worker" or anything else.
|
||||||
|
*
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
const _ = require('lodash');
|
||||||
import { dirname } from "node:path";
|
const logger = require('../logger').access;
|
||||||
import { fileURLToPath } from "node:url";
|
const validator = require('ajv');
|
||||||
import Ajv from "ajv/dist/2020.js";
|
const error = require('./error');
|
||||||
import _ from "lodash";
|
const userModel = require('../models/user');
|
||||||
import { access as logger } from "../logger.js";
|
const proxyHostModel = require('../models/proxy_host');
|
||||||
import authModel from "../models/auth.js";
|
const TokenModel = require('../models/token');
|
||||||
import proxyHostModel, { scopeToOwnerOrAllowed } from "../models/proxy_host.js";
|
const roleSchema = require('./access/roles.json');
|
||||||
import TokenModel from "../models/token.js";
|
const permsSchema = require('./access/permissions.json');
|
||||||
import userModel from "../models/user.js";
|
|
||||||
import permsSchema from "./access/permissions.json" with { type: "json" };
|
|
||||||
import roleSchema from "./access/roles.json" with { type: "json" };
|
|
||||||
import errs from "./error.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
module.exports = function (token_string) {
|
||||||
const __dirname = dirname(__filename);
|
let Token = new TokenModel();
|
||||||
|
let token_data = null;
|
||||||
export default function (tokenString) {
|
let initialised = false;
|
||||||
const Token = TokenModel();
|
let object_cache = {};
|
||||||
let tokenData = null;
|
let allow_internal_access = false;
|
||||||
let initialised = false;
|
let user_roles = [];
|
||||||
const objectCache = {};
|
let permissions = {};
|
||||||
let allowInternalAccess = false;
|
|
||||||
let userRoles = [];
|
|
||||||
let permissions = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the Token object from the token string
|
* Loads the Token object from the token string
|
||||||
*
|
*
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
this.init = async () => {
|
this.init = () => {
|
||||||
if (initialised) {
|
return new Promise((resolve, reject) => {
|
||||||
return;
|
if (initialised) {
|
||||||
}
|
resolve();
|
||||||
|
} else if (!token_string) {
|
||||||
if (!tokenString) {
|
reject(new error.PermissionError('Permission Denied'));
|
||||||
throw new errs.PermissionError("Permission Denied");
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenData = await Token.load(tokenString);
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
tokenData.attrs.id ||
|
|
||||||
(typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, "user") !== -1)
|
|
||||||
) {
|
|
||||||
// Has token user id or token user scope
|
|
||||||
const user = await userModel
|
|
||||||
.query()
|
|
||||||
.where("id", tokenData.attrs.id)
|
|
||||||
.andWhere("is_deleted", 0)
|
|
||||||
.andWhere("is_disabled", 0)
|
|
||||||
.allowGraph("[permissions]")
|
|
||||||
.withGraphFetched("[permissions]")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
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 ok = true;
|
|
||||||
_.forEach(tokenData.scope, (scope_item) => {
|
|
||||||
if (_.indexOf(user.roles, scope_item) === -1) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ok) {
|
|
||||||
throw new errs.AuthError("Invalid token scope for User");
|
|
||||||
}
|
|
||||||
|
|
||||||
// A token issued before the password was last changed is no longer valid: taking an account
|
|
||||||
// back from whoever has the old password has to end the sessions that password opened.
|
|
||||||
const auth = await authModel
|
|
||||||
.query()
|
|
||||||
.where("user_id", "=", user.id)
|
|
||||||
.where("type", "=", "password")
|
|
||||||
.first();
|
|
||||||
|
|
||||||
// Both sides come from the same clock and in the same unit, whole seconds since
|
|
||||||
// the epoch: `setPassword` stamps the marker and `jsonwebtoken` stamps `iat`.
|
|
||||||
const changedAt = auth?.meta?.password_changed_at;
|
|
||||||
if (changedAt && typeof tokenData.iat === "number" && tokenData.iat < changedAt) {
|
|
||||||
throw new errs.TokenRevokedError("Token was issued before the password was changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
initialised = true;
|
|
||||||
userRoles = user.roles;
|
|
||||||
permissions = user.permissions;
|
|
||||||
} else {
|
} else {
|
||||||
throw new errs.AuthError("User cannot be loaded for Token");
|
resolve(Token.load(token_string)
|
||||||
|
.then((data) => {
|
||||||
|
token_data = data;
|
||||||
|
|
||||||
|
// At this point we need to load the user from the DB and make sure they:
|
||||||
|
// - exist (and not soft deleted)
|
||||||
|
// - still have the appropriate scopes for this token
|
||||||
|
// This is only required when the User ID is supplied or if the token scope has `user`
|
||||||
|
|
||||||
|
if (token_data.attrs.id || (typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'user') !== -1)) {
|
||||||
|
// Has token user id or token user scope
|
||||||
|
return userModel
|
||||||
|
.query()
|
||||||
|
.where('id', token_data.attrs.id)
|
||||||
|
.andWhere('is_deleted', 0)
|
||||||
|
.andWhere('is_disabled', 0)
|
||||||
|
.allowEager('[permissions]')
|
||||||
|
.eager('[permissions]')
|
||||||
|
.first()
|
||||||
|
.then((user) => {
|
||||||
|
if (user) {
|
||||||
|
// make sure user has all scopes of the token
|
||||||
|
// The `user` role is not added against the user row, so we have to just add it here to get past this check.
|
||||||
|
user.roles.push('user');
|
||||||
|
|
||||||
|
let is_ok = true;
|
||||||
|
_.forEach(token_data.scope, (scope_item) => {
|
||||||
|
if (_.indexOf(user.roles, scope_item) === -1) {
|
||||||
|
is_ok = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!is_ok) {
|
||||||
|
throw new error.AuthError('Invalid token scope for User');
|
||||||
|
} else {
|
||||||
|
initialised = true;
|
||||||
|
user_roles = user.roles;
|
||||||
|
permissions = user.permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new error.AuthError('User cannot be loaded for Token');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
initialised = true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
initialised = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,124 +96,141 @@ export default function (tokenString) {
|
|||||||
* This only applies to USER token scopes, as all other tokens are not really bound
|
* This only applies to USER token scopes, as all other tokens are not really bound
|
||||||
* by object scopes
|
* by object scopes
|
||||||
*
|
*
|
||||||
* @param {String} objectType
|
* @param {String} object_type
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
this.loadObjects = async (objectType) => {
|
this.loadObjects = (object_type) => {
|
||||||
let objects = null;
|
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 (Token.hasScope("user")) {
|
if (typeof object_cache[object_type] === 'undefined') {
|
||||||
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) {
|
switch (object_type) {
|
||||||
throw new errs.AuthError("User Token supplied without a User ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenUserId = tokenData.attrs.id ? tokenData.attrs.id : 0;
|
// USERS - should only return yourself
|
||||||
|
case 'users':
|
||||||
|
resolve(token_user_id ? [token_user_id] : []);
|
||||||
|
break;
|
||||||
|
|
||||||
if (typeof objectCache[objectType] !== "undefined") {
|
// Proxy Hosts
|
||||||
objects = objectCache[objectType];
|
case 'proxy_hosts':
|
||||||
} else {
|
query = proxyHostModel
|
||||||
switch (objectType) {
|
.query()
|
||||||
// USERS - should only return yourself
|
.select('id')
|
||||||
case "users":
|
.andWhere('is_deleted', 0);
|
||||||
objects = tokenUserId ? [tokenUserId] : [];
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Proxy Hosts
|
if (permissions.visibility === 'user') {
|
||||||
case "proxy_hosts": {
|
query.andWhere('owner_user_id', token_user_id);
|
||||||
const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
|
}
|
||||||
|
|
||||||
if (permissions.visibility === "user") {
|
resolve(query
|
||||||
scopeToOwnerOrAllowed(query, tokenUserId, permissions.meta);
|
.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 {
|
||||||
const rows = await query;
|
resolve(object_cache[object_type]);
|
||||||
objects = [];
|
|
||||||
_.forEach(rows, (ruleRow) => {
|
|
||||||
objects.push(ruleRow.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// enum should not have less than 1 item
|
|
||||||
if (!objects.length) {
|
|
||||||
objects.push(0);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objectCache[objectType] = objects;
|
} else {
|
||||||
|
resolve(null);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
return objects;
|
.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
|
* Creates a schema object on the fly with the IDs and other values required to be checked against the permissionSchema
|
||||||
*
|
*
|
||||||
* @param {String} permissionLabel
|
* @param {String} permission_label
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
this.getObjectSchema = async (permissionLabel) => {
|
this.getObjectSchema = (permission_label) => {
|
||||||
const baseObjectType = permissionLabel.split(":").shift();
|
let base_object_type = permission_label.split(':').shift();
|
||||||
|
|
||||||
const schema = {
|
let schema = {
|
||||||
$id: "objects",
|
$id: 'objects',
|
||||||
description: "Actor Properties",
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
type: "object",
|
description: 'Actor Properties',
|
||||||
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
properties: {
|
properties: {
|
||||||
user_id: {
|
user_id: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{
|
{
|
||||||
type: "number",
|
type: 'number',
|
||||||
enum: [Token.get("attrs").id],
|
enum: [Token.get('attrs').id]
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
scope: {
|
scope: {
|
||||||
type: "string",
|
type: 'string',
|
||||||
pattern: `^${Token.get("scope")}$`,
|
pattern: '^' + Token.get('scope') + '$'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.loadObjects(baseObjectType);
|
return this.loadObjects(base_object_type)
|
||||||
if (typeof result === "object" && result !== null) {
|
.then((object_result) => {
|
||||||
schema.properties[baseObjectType] = {
|
if (typeof object_result === 'object' && object_result !== null) {
|
||||||
type: "number",
|
schema.properties[base_object_type] = {
|
||||||
enum: result,
|
type: 'number',
|
||||||
minimum: 1,
|
enum: object_result,
|
||||||
};
|
minimum: 1
|
||||||
} else {
|
};
|
||||||
schema.properties[baseObjectType] = {
|
} else {
|
||||||
type: "number",
|
schema.properties[base_object_type] = {
|
||||||
minimum: 1,
|
type: 'number',
|
||||||
};
|
minimum: 1
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// here:
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: Token,
|
|
||||||
|
|
||||||
getPermissions: () => permissions,
|
token: Token,
|
||||||
getRoles: () => userRoles,
|
|
||||||
hasRole: (role) => userRoles.includes(role),
|
|
||||||
isAdmin: () => userRoles.includes("admin") || Token.hasScope("admin"),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Boolean} [allowInternal]
|
* @param {Boolean} [allow_internal]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
load: async (allowInternal) => {
|
load: (allow_internal) => {
|
||||||
if (tokenString) {
|
return new Promise(function (resolve/*, reject*/) {
|
||||||
await this.init();
|
if (token_string) {
|
||||||
return tokenData;
|
resolve(Token.load(token_string));
|
||||||
}
|
} else {
|
||||||
allowInternalAccess = allowInternal;
|
allow_internal_access = allow_internal;
|
||||||
return allowInternal || null;
|
resolve(allow_internal_access || null);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
reloadObjects: this.loadObjects,
|
reloadObjects: this.loadObjects,
|
||||||
@@ -240,65 +241,74 @@ export default function (tokenString) {
|
|||||||
* @param {*} [data]
|
* @param {*} [data]
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
can: async (permission, data) => {
|
can: (permission, data) => {
|
||||||
if (allowInternalAccess === true) {
|
if (allow_internal_access === true) {
|
||||||
return true;
|
return Promise.resolve(true);
|
||||||
|
//return true;
|
||||||
|
} else {
|
||||||
|
return this.init()
|
||||||
|
.then(() => {
|
||||||
|
// Initialised, token decoded ok
|
||||||
|
return this.getObjectSchema(permission)
|
||||||
|
.then((objectSchema) => {
|
||||||
|
let data_schema = {
|
||||||
|
[permission]: {
|
||||||
|
data: data,
|
||||||
|
scope: Token.get('scope'),
|
||||||
|
roles: user_roles,
|
||||||
|
permission_visibility: permissions.visibility,
|
||||||
|
permission_proxy_hosts: permissions.proxy_hosts,
|
||||||
|
permission_redirection_hosts: permissions.redirection_hosts,
|
||||||
|
permission_dead_hosts: permissions.dead_hosts,
|
||||||
|
permission_streams: permissions.streams,
|
||||||
|
permission_access_lists: permissions.access_lists,
|
||||||
|
permission_certificates: permissions.certificates
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let permissionSchema = {
|
||||||
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
$async: true,
|
||||||
|
$id: 'permissions',
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
|
||||||
|
|
||||||
|
// logger.info('objectSchema', JSON.stringify(objectSchema, null, 2));
|
||||||
|
// logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2));
|
||||||
|
// logger.info('data_schema', JSON.stringify(data_schema, null, 2));
|
||||||
|
|
||||||
|
let ajv = validator({
|
||||||
|
verbose: true,
|
||||||
|
allErrors: true,
|
||||||
|
format: 'full',
|
||||||
|
missingRefs: 'fail',
|
||||||
|
breakOnError: true,
|
||||||
|
coerceTypes: true,
|
||||||
|
schemas: [
|
||||||
|
roleSchema,
|
||||||
|
permsSchema,
|
||||||
|
objectSchema,
|
||||||
|
permissionSchema
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return ajv.validate('permissions', data_schema)
|
||||||
|
.then(() => {
|
||||||
|
return data_schema[permission];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
err.permission = permission;
|
||||||
|
err.permission_data = data;
|
||||||
|
logger.error(permission, data, err.message);
|
||||||
|
|
||||||
|
throw new error.PermissionError('Permission Denied', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
|
||||||
await this.init();
|
|
||||||
const objectSchema = await this.getObjectSchema(permission);
|
|
||||||
|
|
||||||
const dataSchema = {
|
|
||||||
[permission]: {
|
|
||||||
data: data,
|
|
||||||
scope: Token.get("scope"),
|
|
||||||
roles: userRoles,
|
|
||||||
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,
|
|
||||||
permission_meta: permissions.meta,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const permissionSchema = {
|
|
||||||
$async: true,
|
|
||||||
$id: "permissions",
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawData = fs.readFileSync(`${__dirname}/access/${permission.replace(/:/gim, "-")}.json`, {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
permissionSchema.properties[permission] = JSON.parse(rawData);
|
|
||||||
|
|
||||||
const ajv = new Ajv({
|
|
||||||
verbose: true,
|
|
||||||
allErrors: true,
|
|
||||||
breakOnError: true,
|
|
||||||
coerceTypes: true,
|
|
||||||
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
|
|
||||||
});
|
|
||||||
|
|
||||||
const valid = await ajv.validate("permissions", dataSchema);
|
|
||||||
return valid && dataSchema[permission];
|
|
||||||
} catch (err) {
|
|
||||||
err.permission = permission;
|
|
||||||
err.permission_data = data;
|
|
||||||
logger.error(permission, data, err.message);
|
|
||||||
// A revoked token is not a permission problem, and the client can tell: the frontend
|
|
||||||
// clears the session on a 401 and on nothing else.
|
|
||||||
if (err instanceof errs.TokenRevokedError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
throw new errs.PermissionError("Permission Denied", err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"$ref": "roles#/definitions/admin"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$id": "perms",
|
"$id": "perms",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"view": {
|
"view": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
"$id": "roles",
|
"$id": "roles",
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"admin": {
|
"admin": {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "roles#/definitions/admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["permission_ssl_passthrough_hosts", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"permission_ssl_passthrough_hosts": {
|
||||||
|
"$ref": "perms#/definitions/manage"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "roles#/definitions/admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["permission_ssl_passthrough_hosts", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"permission_ssl_passthrough_hosts": {
|
||||||
|
"$ref": "perms#/definitions/manage"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "roles#/definitions/admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["permission_ssl_passthrough_hosts", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"permission_ssl_passthrough_hosts": {
|
||||||
|
"$ref": "perms#/definitions/view"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "roles#/definitions/admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["permission_ssl_passthrough_hosts", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"permission_ssl_passthrough_hosts": {
|
||||||
|
"$ref": "perms#/definitions/view"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "roles#/definitions/admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["permission_ssl_passthrough_hosts", "roles"],
|
||||||
|
"properties": {
|
||||||
|
"permission_ssl_passthrough_hosts": {
|
||||||
|
"$ref": "perms#/definitions/manage"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import batchflow from "batchflow";
|
|
||||||
import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" };
|
|
||||||
import { certbot as logger } from "../logger.js";
|
|
||||||
import errs from "./error.js";
|
|
||||||
import utils from "./utils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a cerbot plugin given the key for the object from
|
|
||||||
* ../certbot/dns-plugins.json
|
|
||||||
*
|
|
||||||
* @param {string} pluginKey
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
const installPlugin = async (pluginKey) => {
|
|
||||||
if (typeof dnsPlugins[pluginKey] === "undefined") {
|
|
||||||
throw new errs.ItemNotFoundError(pluginKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin = dnsPlugins[pluginKey];
|
|
||||||
logger.start(`Installing ${pluginKey}...`);
|
|
||||||
|
|
||||||
plugin.version = plugin.version.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
|
|
||||||
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, process.env.CERTBOT_VERSION);
|
|
||||||
|
|
||||||
// SETUPTOOLS_USE_DISTUTILS=local uses setuptools' own bundled distutils.
|
|
||||||
// "stdlib" breaks Python 3.13+ where distutils was removed from the standard library.
|
|
||||||
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "local" });
|
|
||||||
if (typeof plugin.env === "object") {
|
|
||||||
env = Object.assign(env, plugin.env);
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotedDeps = plugin.dependencies.trim()
|
|
||||||
? plugin.dependencies
|
|
||||||
.trim()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((d) => `'${d}'`)
|
|
||||||
.join(" ")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${quotedDeps} '${plugin.package_name}${plugin.version}' && deactivate`;
|
|
||||||
return utils
|
|
||||||
.exec(cmd, { env })
|
|
||||||
.then((result) => {
|
|
||||||
logger.complete(`Installed ${pluginKey}`);
|
|
||||||
return result;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {array} pluginKeys
|
|
||||||
*/
|
|
||||||
const installPlugins = async (pluginKeys) => {
|
|
||||||
let hasErrors = false;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (pluginKeys.length === 0) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
batchflow(pluginKeys)
|
|
||||||
.sequential()
|
|
||||||
.each((_i, pluginKey, next) => {
|
|
||||||
installPlugin(pluginKey)
|
|
||||||
.then(() => {
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
hasErrors = true;
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.error((err) => {
|
|
||||||
logger.error(err.message);
|
|
||||||
})
|
|
||||||
.end(() => {
|
|
||||||
if (hasErrors) {
|
|
||||||
reject(new errs.CommandError("Some plugins failed to install. Please check the logs above", 1));
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { installPlugin, installPlugins };
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import NodeRSA from "node-rsa";
|
|
||||||
import { global as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const keysFile = "/data/keys.json";
|
|
||||||
const mysqlEngine = "mysql2";
|
|
||||||
const postgresEngine = "pg";
|
|
||||||
const sqliteClientName = "better-sqlite3";
|
|
||||||
|
|
||||||
// Not used for new setups anymore but may exist in legacy setups
|
|
||||||
const legacySqliteClientName = "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 {
|
|
||||||
// Load this json synchronously
|
|
||||||
const rawData = fs.readFileSync(filename);
|
|
||||||
configData = JSON.parse(rawData);
|
|
||||||
} catch (_) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configData?.database) {
|
|
||||||
logger.info(`Using configuration from file: ${filename}`);
|
|
||||||
|
|
||||||
// Migrate those who have "mysql" engine to "mysql2"
|
|
||||||
if (configData.database.engine === "mysql") {
|
|
||||||
configData.database.engine = mysqlEngine;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = configData;
|
|
||||||
instance.keys = getKeys();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toBool = (v) => /^(1|true|yes|on)$/i.test((v || "").trim());
|
|
||||||
|
|
||||||
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
|
|
||||||
const envMysqlUser = process.env.DB_MYSQL_USER || null;
|
|
||||||
const envMysqlName = process.env.DB_MYSQL_NAME || null;
|
|
||||||
const envMysqlSSL = toBool(process.env.DB_MYSQL_SSL);
|
|
||||||
const envMysqlSSLRejectUnauthorized =
|
|
||||||
process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED === undefined
|
|
||||||
? true
|
|
||||||
: toBool(process.env.DB_MYSQL_SSL_REJECT_UNAUTHORIZED);
|
|
||||||
const envMysqlSSLVerifyIdentity =
|
|
||||||
process.env.DB_MYSQL_SSL_VERIFY_IDENTITY === undefined
|
|
||||||
? true
|
|
||||||
: toBool(process.env.DB_MYSQL_SSL_VERIFY_IDENTITY);
|
|
||||||
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,
|
|
||||||
ssl: envMysqlSSL
|
|
||||||
? { rejectUnauthorized: envMysqlSSLRejectUnauthorized, verifyIdentity: envMysqlSSLVerifyIdentity }
|
|
||||||
: false,
|
|
||||||
},
|
|
||||||
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 (isDebugMode()) {
|
|
||||||
logger.debug("Checking for keys file:", keysFile);
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(keysFile)) {
|
|
||||||
generateKeys();
|
|
||||||
} else if (process.env.DEBUG) {
|
|
||||||
logger.info("Keys file exists OK");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Load this json keysFile synchronously and return the json object
|
|
||||||
const rawData = fs.readFileSync(keysFile);
|
|
||||||
return JSON.parse(rawData);
|
|
||||||
} 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), { mode: 0o600 });
|
|
||||||
} 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}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {string} key ie: 'database' or 'database.engine'
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const configHas = (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 {*}
|
|
||||||
*/
|
|
||||||
const configGet = (key) => {
|
|
||||||
instance === null && configure();
|
|
||||||
if (key && typeof instance[key] !== "undefined") {
|
|
||||||
return instance[key];
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this a sqlite configuration?
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isSqlite = () => {
|
|
||||||
instance === null && configure();
|
|
||||||
return instance.database.knex && [sqliteClientName, legacySqliteClientName].includes(instance.database.knex.client);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this a mysql configuration?
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isMysql = () => {
|
|
||||||
instance === null && configure();
|
|
||||||
return instance.database.engine === mysqlEngine;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this a postgres configuration?
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isPostgres = () => {
|
|
||||||
instance === null && configure();
|
|
||||||
return instance.database.engine === postgresEngine;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running in debug mode?
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isDebugMode = () => !!process.env.DEBUG;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running in CI?
|
|
||||||
*
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const isCI = () => process.env.CI === "true" && process.env.DEBUG === "true";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a public key
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
const getPublicKey = () => {
|
|
||||||
instance === null && configure();
|
|
||||||
return instance.keys.pub;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a private key
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
const getPrivateKey = () => {
|
|
||||||
instance === null && configure();
|
|
||||||
return instance.keys.key;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
const useLetsencryptStaging = () => !!process.env.LE_STAGING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string|null}
|
|
||||||
*/
|
|
||||||
const useLetsencryptServer = () => {
|
|
||||||
if (process.env.LE_SERVER) {
|
|
||||||
return process.env.LE_SERVER;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
isCI,
|
|
||||||
configHas,
|
|
||||||
configGet,
|
|
||||||
isSqlite,
|
|
||||||
isMysql,
|
|
||||||
isPostgres,
|
|
||||||
isDebugMode,
|
|
||||||
getPrivateKey,
|
|
||||||
getPublicKey,
|
|
||||||
useLetsencryptStaging,
|
|
||||||
useLetsencryptServer,
|
|
||||||
};
|
|
||||||
+45
-67
@@ -1,112 +1,90 @@
|
|||||||
import _ from "lodash";
|
const _ = require('lodash');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
const errs = {
|
module.exports = {
|
||||||
PermissionError: function (_, previous) {
|
|
||||||
|
PermissionError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = "Permission Denied";
|
this.message = 'Permission Denied';
|
||||||
this.public = true;
|
this.public = true;
|
||||||
this.status = 403;
|
this.status = 403;
|
||||||
},
|
},
|
||||||
|
|
||||||
ItemNotFoundError: function (id, previous) {
|
ItemNotFoundError: function (id, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = "Not Found";
|
this.message = 'Item Not Found - ' + id;
|
||||||
if (id) {
|
this.public = true;
|
||||||
this.message = `Not Found - ${id}`;
|
this.status = 404;
|
||||||
}
|
|
||||||
this.public = true;
|
|
||||||
this.status = 404;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
TokenRevokedError: function (message, previous) {
|
AuthError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.public = true;
|
this.public = true;
|
||||||
this.status = 401;
|
this.status = 401;
|
||||||
},
|
|
||||||
|
|
||||||
AuthError: function (message, messageI18n, previous) {
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
this.previous = previous;
|
|
||||||
this.message = message;
|
|
||||||
this.message_i18n = messageI18n;
|
|
||||||
this.public = true;
|
|
||||||
this.status = 400;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
InternalError: function (message, previous) {
|
InternalError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.status = 500;
|
this.status = 500;
|
||||||
this.public = false;
|
this.public = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
InternalValidationError: function (message, previous) {
|
InternalValidationError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.status = 400;
|
this.status = 400;
|
||||||
this.public = false;
|
this.public = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
ConfigurationError: function (message, previous) {
|
ConfigurationError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.status = 400;
|
this.status = 400;
|
||||||
this.public = true;
|
this.public = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
CacheError: function (message, previous) {
|
CacheError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.status = 500;
|
this.status = 500;
|
||||||
this.public = false;
|
this.public = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
ValidationError: function (message, previous) {
|
ValidationError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.public = true;
|
this.public = true;
|
||||||
this.status = 400;
|
this.status = 400;
|
||||||
},
|
},
|
||||||
|
|
||||||
AssertionFailedError: function (message, previous) {
|
AssertionFailedError: function (message, previous) {
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.previous = previous;
|
this.previous = previous;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.public = false;
|
this.public = false;
|
||||||
this.status = 400;
|
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(errs, (err) => {
|
_.forEach(module.exports, function (error) {
|
||||||
err.prototype = Object.create(Error.prototype);
|
util.inherits(error, Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default errs;
|
|
||||||
+34
-11
@@ -1,17 +1,40 @@
|
|||||||
export default (req, res, next) => {
|
const validator = require('../validator');
|
||||||
|
|
||||||
|
module.exports = function (req, res, next) {
|
||||||
|
|
||||||
if (req.headers.origin) {
|
if (req.headers.origin) {
|
||||||
res.set({
|
|
||||||
"Access-Control-Allow-Origin": req.headers.origin,
|
const originSchema = {
|
||||||
"Access-Control-Allow-Credentials": true,
|
oneOf: [
|
||||||
"Access-Control-Allow-Methods": "OPTIONS, GET, POST, PUT, DELETE",
|
{
|
||||||
"Access-Control-Allow-Headers":
|
type: 'string',
|
||||||
"Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
|
pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
|
||||||
"Access-Control-Max-Age": 5 * 60,
|
},
|
||||||
"Access-Control-Expose-Headers": "X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit",
|
{
|
||||||
});
|
type: 'string',
|
||||||
next();
|
pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// very relaxed validation....
|
||||||
|
validator(originSchema, req.headers.origin)
|
||||||
|
.then(function () {
|
||||||
|
res.set({
|
||||||
|
'Access-Control-Allow-Origin': req.headers.origin,
|
||||||
|
'Access-Control-Allow-Credentials': true,
|
||||||
|
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
|
||||||
|
'Access-Control-Max-Age': 5 * 60,
|
||||||
|
'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// No origin
|
// No origin
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import Access from "../access.js";
|
const Access = require('../access');
|
||||||
|
|
||||||
export default () => {
|
module.exports = () => {
|
||||||
return async (_, res, next) => {
|
return function (req, res, next) {
|
||||||
try {
|
res.locals.access = null;
|
||||||
res.locals.access = null;
|
let access = new Access(res.locals.token || null);
|
||||||
const access = new Access(res.locals.token || null);
|
access.load()
|
||||||
await access.load();
|
.then(() => {
|
||||||
res.locals.access = access;
|
res.locals.access = access;
|
||||||
next();
|
next();
|
||||||
} catch (err) {
|
})
|
||||||
next(err);
|
.catch(next);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
export default function () {
|
module.exports = function () {
|
||||||
return (req, res, next) => {
|
return function (req, res, next) {
|
||||||
if (req.headers.authorization) {
|
if (req.headers.authorization) {
|
||||||
const parts = req.headers.authorization.split(" ");
|
let parts = req.headers.authorization.split(' ');
|
||||||
|
|
||||||
if (parts && parts[0] === "Bearer" && parts[1]) {
|
if (parts && parts[0] === 'Bearer' && parts[1]) {
|
||||||
res.locals.token = parts[1];
|
res.locals.token = parts[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import { debug, express as logger } from "../../logger.js";
|
|
||||||
|
|
||||||
export default (req, _res, next) => {
|
|
||||||
debug(logger, `[${chalk.yellow(req.method.toUpperCase())}] ${chalk.green(req.path)}`);
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from "lodash";
|
let _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = function (default_sort, default_offset, default_limit, max_limit) {
|
||||||
|
|
||||||
export default (default_sort, default_offset, default_limit, max_limit) => {
|
|
||||||
/**
|
/**
|
||||||
* This will setup the req query params with filtered data and defaults
|
* This will setup the req query params with filtered data and defaults
|
||||||
*
|
*
|
||||||
@@ -10,35 +11,34 @@ export default (default_sort, default_offset, default_limit, max_limit) => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return (req, _res, next) => {
|
return function (req, res, next) {
|
||||||
req.query.offset =
|
|
||||||
typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10);
|
req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
|
||||||
req.query.limit =
|
req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);
|
||||||
typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);
|
|
||||||
|
|
||||||
if (max_limit && req.query.limit > max_limit) {
|
if (max_limit && req.query.limit > max_limit) {
|
||||||
req.query.limit = max_limit;
|
req.query.limit = max_limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort;
|
let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
|
||||||
const myRegexp = /.*\.(asc|desc)$/gi;
|
let myRegexp = /.*\.(asc|desc)$/ig;
|
||||||
const sort_array = [];
|
let sort_array = [];
|
||||||
|
|
||||||
sort = sort.split(",");
|
sort = sort.split(',');
|
||||||
_.map(sort, (val) => {
|
_.map(sort, function (val) {
|
||||||
const matches = myRegexp.exec(val);
|
let matches = myRegexp.exec(val);
|
||||||
|
|
||||||
if (matches !== null) {
|
if (matches !== null) {
|
||||||
const dir = matches[1];
|
let dir = matches[1];
|
||||||
sort_array.push({
|
sort_array.push({
|
||||||
field: val.substr(0, val.length - (dir.length + 1)),
|
field: val.substr(0, val.length - (dir.length + 1)),
|
||||||
dir: dir.toLowerCase(),
|
dir: dir.toLowerCase()
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sort_array.push({
|
sort_array.push({
|
||||||
field: val,
|
field: val,
|
||||||
dir: "asc",
|
dir: 'asc'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import { isCI } from "../config.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-memory fixed window rate limiter for authentication endpoints
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Keys longer than this are hashed, so a client can't make the store hold arbitrarily large
|
|
||||||
// strings (keys can include request body values such as the login identity).
|
|
||||||
const MAX_KEY_LENGTH = 128;
|
|
||||||
// Entries per limiter. A flood of unique keys sweeps expired entries, then evicts the oldest,
|
|
||||||
// instead of growing the store until the periodic cleanup runs.
|
|
||||||
const MAX_STORE_SIZE = 50000;
|
|
||||||
|
|
||||||
// One store per limiter, so separate endpoints never share buckets
|
|
||||||
const stores = new Set();
|
|
||||||
|
|
||||||
// Clean up old entries every 2 minutes
|
|
||||||
const cleanupTimer = setInterval(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
for (const store of stores) {
|
|
||||||
for (const [key, record] of store.entries()) {
|
|
||||||
if (now - record.startTime > record.windowMs * 2) {
|
|
||||||
store.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 120000);
|
|
||||||
cleanupTimer.unref();
|
|
||||||
|
|
||||||
// Generated at container start (prepare/45-admin-port.sh) and sent by the admin nginx as
|
|
||||||
// X-NPM-Peer-Secret alongside X-NPM-Peer-Addr. Unset (development, or a mounted production.conf)
|
|
||||||
// means the header is never trusted.
|
|
||||||
const peerSecret = Buffer.from(process.env.NPM_PEER_SECRET || "");
|
|
||||||
|
|
||||||
const hasPeerSecret = (value) => {
|
|
||||||
if (!peerSecret.length || typeof value !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const given = Buffer.from(value);
|
|
||||||
return given.length === peerSecret.length && crypto.timingSafeEqual(given, peerSecret);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The address to rate limit on: the TCP peer of the admin nginx, which it passes as
|
|
||||||
* X-NPM-Peer-Addr ($realip_remote_addr). That header is only honoured from a loopback socket
|
|
||||||
* carrying the startup secret, so it can only have come from the admin nginx: a proxy host or
|
|
||||||
* stream forwarding to the backend port is also a loopback peer, but can't know the secret.
|
|
||||||
* The realip-resolved client address is deliberately not used: nginx trusts X-Real-IP from
|
|
||||||
* private ranges, so a client on the LAN or Docker network could pick a new address, and a fresh
|
|
||||||
* bucket, on every request. Behind another proxy (including an NPM proxy host in front of the
|
|
||||||
* admin UI) this resolves to that proxy's address.
|
|
||||||
*
|
|
||||||
* @param {Object} req
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
export const getClientIp = (req) => {
|
|
||||||
const socketAddr = req.socket?.remoteAddress || "unknown";
|
|
||||||
const isLoopback = socketAddr === "127.0.0.1" || socketAddr === "::1" || socketAddr === "::ffff:127.0.0.1";
|
|
||||||
const trusted = isLoopback && hasPeerSecret(req.headers["x-npm-peer-secret"]);
|
|
||||||
const peerAddr = trusted ? req.headers["x-npm-peer-addr"]?.trim() : null;
|
|
||||||
return peerAddr || socketAddr;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeRoom = (store, now) => {
|
|
||||||
for (const [k, r] of store.entries()) {
|
|
||||||
if (now - r.startTime > r.windowMs) {
|
|
||||||
store.delete(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Maps iterate in insertion order, so this drops the oldest windows first
|
|
||||||
for (const k of store.keys()) {
|
|
||||||
if (store.size < MAX_STORE_SIZE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
store.delete(k);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const boundedKey = (key) =>
|
|
||||||
key.length > MAX_KEY_LENGTH ? `sha256:${crypto.createHash("sha256").update(key).digest("hex")}` : key;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} [options]
|
|
||||||
* @param {Number} [options.windowMs]
|
|
||||||
* @param {Number} [options.max]
|
|
||||||
* @param {String} [options.message]
|
|
||||||
* @param {Boolean} [options.skipSuccessfulRequests]
|
|
||||||
* @param {Function} [options.keyGenerator] (req, clientIp) => String. Defaults to the client IP.
|
|
||||||
* Including the target account in the key stops every
|
|
||||||
* user being locked out when many clients share an IP
|
|
||||||
* (e.g. the admin UI behind another reverse proxy).
|
|
||||||
*/
|
|
||||||
export default function createRateLimiter(options = {}) {
|
|
||||||
const windowMs = options.windowMs || 60 * 1000; // 1 minute default
|
|
||||||
const max = options.max || 10; // 10 requests default
|
|
||||||
const message = options.message || "Too many attempts from this IP, please try again later.";
|
|
||||||
const skipSuccessfulRequests = options.skipSuccessfulRequests !== false;
|
|
||||||
const keyGenerator = options.keyGenerator || ((_req, clientIp) => clientIp);
|
|
||||||
|
|
||||||
const store = new Map();
|
|
||||||
stores.add(store);
|
|
||||||
|
|
||||||
return (req, res, next) => {
|
|
||||||
// Bypass rate limiting in CI, test environments, or when explicitly disabled
|
|
||||||
if (
|
|
||||||
isCI() ||
|
|
||||||
process.env.CI === "true" ||
|
|
||||||
process.env.NODE_ENV === "test" ||
|
|
||||||
process.env.DISABLE_RATE_LIMIT === "true"
|
|
||||||
) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = boundedKey(String(keyGenerator(req, getClientIp(req))));
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
let record = store.get(key);
|
|
||||||
|
|
||||||
if (!record || now - record.startTime > windowMs) {
|
|
||||||
record = {
|
|
||||||
count: 0,
|
|
||||||
startTime: now,
|
|
||||||
windowMs,
|
|
||||||
};
|
|
||||||
if (!store.has(key) && store.size >= MAX_STORE_SIZE) {
|
|
||||||
makeRoom(store, now);
|
|
||||||
}
|
|
||||||
store.set(key, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
record.count++;
|
|
||||||
|
|
||||||
if (skipSuccessfulRequests) {
|
|
||||||
const currentRecord = record;
|
|
||||||
res.on("finish", () => {
|
|
||||||
if (res.statusCode < 400 && currentRecord.count > 0) {
|
|
||||||
currentRecord.count--;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.count > max) {
|
|
||||||
res.setHeader("Retry-After", Math.ceil((record.startTime + windowMs - now) / 1000));
|
|
||||||
return res.status(429).json({
|
|
||||||
error: {
|
|
||||||
code: 429,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
export default (req, res, next) => {
|
module.exports = (req, res, next) => {
|
||||||
if (req.params.user_id === "me" && res.locals.access) {
|
if (req.params.user_id === 'me' && res.locals.access) {
|
||||||
req.params.user_id = res.locals.access.token.get("attrs").id;
|
req.params.user_id = res.locals.access.token.get('attrs').id;
|
||||||
} else {
|
} else {
|
||||||
req.params.user_id = Number.parseInt(req.params.user_id, 10);
|
req.params.user_id = parseInt(req.params.user_id, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
+28
-54
@@ -1,58 +1,32 @@
|
|||||||
import moment from "moment";
|
const moment = require('moment');
|
||||||
import { ref } from "objection";
|
|
||||||
import { isPostgres } from "./config.js";
|
|
||||||
|
|
||||||
/**
|
module.exports = {
|
||||||
* Takes an expression such as 30d and returns a moment object of that date in future
|
|
||||||
*
|
/**
|
||||||
* Key Shorthand
|
* Takes an expression such as 30d and returns a moment object of that date in future
|
||||||
* ==================
|
*
|
||||||
* years y
|
* Key Shorthand
|
||||||
* quarters Q
|
* ==================
|
||||||
* months M
|
* years y
|
||||||
* weeks w
|
* quarters Q
|
||||||
* days d
|
* months M
|
||||||
* hours h
|
* weeks w
|
||||||
* minutes m
|
* days d
|
||||||
* seconds s
|
* hours h
|
||||||
* milliseconds ms
|
* minutes m
|
||||||
*
|
* seconds s
|
||||||
* @param {String} expression
|
* milliseconds ms
|
||||||
* @returns {Object}
|
*
|
||||||
*/
|
* @param {String} expression
|
||||||
const parseDatePeriod = (expression) => {
|
* @returns {Object}
|
||||||
const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
|
*/
|
||||||
if (matches) {
|
parseDatePeriod: function (expression) {
|
||||||
return moment().add(matches[1], matches[2]);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertIntFieldsToBool = (obj, fields) => {
|
|
||||||
fields.forEach((field) => {
|
|
||||||
if (typeof obj[field] !== "undefined") {
|
|
||||||
obj[field] = obj[field] === 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertBoolFieldsToInt = (obj, fields) => {
|
|
||||||
fields.forEach((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}
|
|
||||||
*/
|
|
||||||
const castJsonIfNeed = (colName) => (isPostgres() ? ref(colName).castText() : colName);
|
|
||||||
|
|
||||||
export { parseDatePeriod, convertIntFieldsToBool, convertBoolFieldsToInt, castJsonIfNeed };
|
|
||||||
@@ -1,34 +1,33 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'identifier_for_migrate';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "identifier_for_migrate";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
*
|
*
|
||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (_knex) => {
|
exports.up = function (knex, Promise) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
// Create Table example:
|
// Create Table example:
|
||||||
|
|
||||||
/*
|
/*return knex.schema.createTable('notification', (table) => {
|
||||||
return knex.schema.createTable('notification', (table) => {
|
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.string('name').notNull();
|
table.string('name').notNull();
|
||||||
table.string('type').notNull();
|
table.string('type').notNull();
|
||||||
table.integer('created_on').notNull();
|
table.integer('created_on').notNull();
|
||||||
table.integer('modified_on').notNull();
|
table.integer('modified_on').notNull();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
logger.info('[' + migrateName + '] Notification Table created');
|
logger.info('[' + migrate_name + '] Notification Table created');
|
||||||
});
|
});*/
|
||||||
*/
|
|
||||||
|
|
||||||
logger.info(`[${migrateName}] Migrating Up Complete`);
|
logger.info('[' + migrate_name + '] Migrating Up Complete');
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
@@ -36,24 +35,21 @@ const up = (_knex) => {
|
|||||||
/**
|
/**
|
||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
// Drop table example:
|
// Drop table example:
|
||||||
|
|
||||||
/*
|
/*return knex.schema.dropTable('notification')
|
||||||
return knex.schema.dropTable('notification')
|
.then(() => {
|
||||||
.then(() => {
|
logger.info('[' + migrate_name + '] Notification Table dropped');
|
||||||
logger.info(`[${migrateName}] Notification Table dropped`);
|
});*/
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
logger.info(`[${migrateName}] Migrating Down Complete`);
|
logger.info('[' + migrate_name + '] Migrating Down Complete');
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import net from "node:net";
|
|
||||||
import errs from "./error.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rules for user-supplied values that the templates write into nginx config as-is.
|
|
||||||
*
|
|
||||||
* Anything that ends or changes an nginx token (whitespace, ";", "{", "}", quotes, "#" which
|
|
||||||
* starts a comment, and "\" which escapes the next character) would let the value add its own
|
|
||||||
* directives, which only admins may do (advanced_config). The API schema rejects these values
|
|
||||||
* with the same patterns (a test keeps the two in step). generateConfig() checks stored values
|
|
||||||
* again, so rows saved before these rules can't add directives either. That check is only about
|
|
||||||
* nginx structure, not format: a stored value in a format the API no longer accepts, but which
|
|
||||||
* can't add directives, still renders.
|
|
||||||
*
|
|
||||||
* Patterns are strings so they can be compared with the JSON schema; compile them with the "u"
|
|
||||||
* flag, as ajv does.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// One character that can't end or change an unquoted nginx token
|
|
||||||
const TOKEN_CHAR = String.raw`[^\s\p{Cc};{}"'#\\]`;
|
|
||||||
// Host part of a forward host: also no "$" (no variables in the upstream address) and no "/"
|
|
||||||
const HOST_CHARS = String.raw`[^\s\p{Cc};{}"'#\\$/]+`;
|
|
||||||
// The existing domain name rule ("a.b.c", no "*", no empty labels), written so it can't
|
|
||||||
// backtrack exponentially
|
|
||||||
const DOMAIN = String.raw`[^.*]+(?:\.[^.*]+)*\.?[^.]`;
|
|
||||||
|
|
||||||
export const patterns = {
|
|
||||||
// server_name entries. The old character blacklist plus whitespace and control characters.
|
|
||||||
domainName: "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"\\s\\p{Cc}]+$",
|
|
||||||
// proxy_host forward_host and location forward_host: a host, optionally followed by a path
|
|
||||||
// (locations split "host/path" at render time)
|
|
||||||
forwardHost: String.raw`^${HOST_CHARS}(?:/${TOKEN_CHAR}*)?$`,
|
|
||||||
// location forward_path, appended straight after the port
|
|
||||||
forwardPath: String.raw`^${TOKEN_CHAR}*$`,
|
|
||||||
// location path: an optional modifier, then one token, either unquoted (a "\" must escape a
|
|
||||||
// visible character, so it can't swallow the space before "{") or double quoted. A modifier
|
|
||||||
// on its own isn't a location.
|
|
||||||
locationPath: String.raw`^(?!(?:=|~\*?|\^~)$)(?:(?:=|~\*?|\^~) +)?(?:"(?:[^"\\\p{Cc}]|\\[^\p{Cc}])+"|(?:[^\s\p{Cc};{}"'#\\]|\\[^\s\p{Cc}])+)$`,
|
|
||||||
// redirection forward_domain_name, e.g. "example.com:8443/path"
|
|
||||||
redirectDomain: String.raw`^(?=${TOKEN_CHAR}+$)${DOMAIN}$`,
|
|
||||||
// stream forwarding_host when it is a domain name (IPs are matched separately)
|
|
||||||
streamDomain: String.raw`^(?=${HOST_CHARS}$)${DOMAIN}$`,
|
|
||||||
// access list client address: IPv6 with an optional prefix. No zone id ("%eth0"): nginx's
|
|
||||||
// allow/deny rejects them, and the old "%.+" let anything through, including "; allow all"
|
|
||||||
clientIpv6: String.raw`^(?:(?:[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][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::[0-9A-Fa-f]{1,4}){1,2}|:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){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][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){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][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){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][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|(?:[0-9A-Fa-f]{1,4}:)(?:(?::[0-9A-Fa-f]{1,4}){1,6}|(?::[0-9A-Fa-f]{1,4}){0,4}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:)|:(?:(?::[0-9A-Fa-f]{1,4}){1,7}|(?::[0-9A-Fa-f]{1,4}){0,5}:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}|:))(?:/(?:[0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$`,
|
|
||||||
// htpasswd user name: a ":" or a line break would corrupt the file
|
|
||||||
accessUsername: String.raw`^[^:\p{Cc}]+$`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const re = Object.fromEntries(Object.entries(patterns).map(([k, v]) => [k, new RegExp(v, "u")]));
|
|
||||||
const unsafeToken = /[\s\p{Cc};{}"'#\\]/u;
|
|
||||||
const ipv4WithPrefix = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:\/(?:[0-9]|[1-2][0-9]|3[0-2]))?$/;
|
|
||||||
|
|
||||||
export const isValid = (name, value) => typeof value === "string" && re[name].test(value);
|
|
||||||
|
|
||||||
export const isValidClientAddress = (value) =>
|
|
||||||
typeof value === "string" && (value === "all" || ipv4WithPrefix.test(value) || re.clientIpv6.test(value));
|
|
||||||
|
|
||||||
export const isValidStreamHost = (value) =>
|
|
||||||
typeof value === "string" && (net.isIP(value) !== 0 || re.streamDomain.test(value));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown when a stored value can't be written into nginx config safely
|
|
||||||
*
|
|
||||||
* @param {String} message
|
|
||||||
*/
|
|
||||||
export const unsafeValueError = (message) => {
|
|
||||||
const err = new errs.ConfigurationError(message);
|
|
||||||
err.unsafeNginxValue = true;
|
|
||||||
return err;
|
|
||||||
};
|
|
||||||
|
|
||||||
const describe = (value) => JSON.stringify(String(value)).slice(0, 80);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} label e.g. "Proxy Host #3"
|
|
||||||
* @param {Object} [accessList]
|
|
||||||
* @throws {ConfigurationError}
|
|
||||||
*/
|
|
||||||
export const assertAccessListRenderable = (label, accessList) => {
|
|
||||||
for (const client of accessList?.clients || []) {
|
|
||||||
// "address" is rendered unquoted ("allow <address>;")
|
|
||||||
if (typeof client.address !== "string" || unsafeToken.test(client.address)) {
|
|
||||||
throw unsafeValueError(`${label}: access list client address ${describe(client.address)} is not allowed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Can be written into config without ending or changing a token
|
|
||||||
export const isTokenSafe = (value) => typeof value === "string" && value.length > 0 && !unsafeToken.test(value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks every user-supplied value the host's template renders. Call before rendering.
|
|
||||||
*
|
|
||||||
* @param {String} hostType proxy_host, redirection_host, dead_host, stream or default
|
|
||||||
* @param {Object} host with access_list loaded, for proxy hosts
|
|
||||||
* @throws {ConfigurationError}
|
|
||||||
*/
|
|
||||||
export const assertHostRenderable = (hostType, host) => {
|
|
||||||
const label = `${hostType.replace(/_/g, " ")} #${host.id}`;
|
|
||||||
const check = (ok, what, value) => {
|
|
||||||
if (!ok) {
|
|
||||||
throw unsafeValueError(`${label}: ${what} ${describe(value)} is not allowed in nginx config`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hostType !== "stream" && hostType !== "default") {
|
|
||||||
for (const name of host.domain_names || []) {
|
|
||||||
check(isTokenSafe(name), "domain name", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostType === "proxy_host") {
|
|
||||||
check(isTokenSafe(host.forward_host), "forward host", host.forward_host);
|
|
||||||
assertAccessListRenderable(label, host.access_list);
|
|
||||||
for (const loc of host.locations || []) {
|
|
||||||
// The path is several tokens (modifier, then a quoted or unquoted URI): its rule is the
|
|
||||||
// structural one
|
|
||||||
check(isValid("locationPath", loc.path), "location path", loc.path);
|
|
||||||
check(isTokenSafe(loc.forward_host), "location forward host", loc.forward_host);
|
|
||||||
if (loc.forward_path) {
|
|
||||||
check(isTokenSafe(loc.forward_path), "location forward path", loc.forward_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostType === "redirection_host") {
|
|
||||||
check(isTokenSafe(host.forward_domain_name), "forward domain", host.forward_domain_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hostType === "stream") {
|
|
||||||
check(isTokenSafe(host.forwarding_host), "forwarding host", host.forwarding_host);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+15
-102
@@ -1,107 +1,20 @@
|
|||||||
import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process";
|
const exec = require('child_process').exec;
|
||||||
import { dirname } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { Liquid } from "liquidjs";
|
|
||||||
import _ from "lodash";
|
|
||||||
import errs from "./error.js";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
module.exports = {
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const exec = async (cmd, options = {}) => {
|
|
||||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
||||||
const child = nodeExec(cmd, options, (isError, stdout, stderr) => {
|
|
||||||
if (isError) {
|
|
||||||
reject(new errs.CommandError(stderr, isError));
|
|
||||||
} else {
|
|
||||||
resolve({ stdout, stderr });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("error", (e) => {
|
|
||||||
reject(new errs.CommandError(stderr, 1, e));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return stdout;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {String} cmd
|
|
||||||
* @param {Array} args
|
|
||||||
* @param {Object|undefined} options
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const execFile = (cmd, args, options) => {
|
|
||||||
const opts = options || {};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
nodeExecFile(cmd, args, opts, (err, stdout, stderr) => {
|
|
||||||
if (err && typeof err === "object") {
|
|
||||||
reject(new errs.CommandError(stderr, 1, err));
|
|
||||||
} else {
|
|
||||||
resolve(stdout.trim());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in objection query builder
|
|
||||||
*
|
|
||||||
* @param {Array} omissions
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
const omitRow = (omissions) => {
|
|
||||||
/**
|
|
||||||
* @param {Object} row
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
return (row) => {
|
|
||||||
return _.omit(row, omissions);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in objection query builder
|
|
||||||
*
|
|
||||||
* @param {Array} omissions
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
const 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
|
|
||||||
*/
|
|
||||||
const getRenderEngine = () => {
|
|
||||||
const renderEngine = new Liquid({
|
|
||||||
root: `${__dirname}/../templates/`,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* nginxAccessRule expects the object given to have 2 properties:
|
* @param {String} cmd
|
||||||
*
|
* @returns {Promise}
|
||||||
* directive string
|
|
||||||
* address string
|
|
||||||
*/
|
*/
|
||||||
renderEngine.registerFilter("nginxAccessRule", (v) => {
|
exec: function (cmd) {
|
||||||
if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) {
|
return new Promise((resolve, reject) => {
|
||||||
return `${v.directive} ${v.address};`;
|
exec(cmd, function (err, stdout, /*stderr*/) {
|
||||||
}
|
if (err && typeof err === 'object') {
|
||||||
return "";
|
reject(err);
|
||||||
});
|
} else {
|
||||||
|
resolve(stdout.trim());
|
||||||
return renderEngine;
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { exec, execFile, omitRow, omitRows, getRenderEngine };
|
|
||||||
@@ -1,51 +1,45 @@
|
|||||||
import net from "node:net";
|
const error = require('../error');
|
||||||
import Ajv from "ajv/dist/2020.js";
|
const path = require('path');
|
||||||
import errs from "../error.js";
|
const parser = require('json-schema-ref-parser');
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = require('ajv')({
|
||||||
verbose: true,
|
verbose: true,
|
||||||
allErrors: true,
|
validateSchema: true,
|
||||||
allowUnionTypes: true,
|
allErrors: false,
|
||||||
strict: false,
|
format: 'full',
|
||||||
coerceTypes: true,
|
coerceTypes: true
|
||||||
});
|
});
|
||||||
|
|
||||||
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} schema
|
* @param {Object} schema
|
||||||
* @param {Object} payload
|
* @param {Object} payload
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const apiValidator = async (schema, payload /*, description*/) => {
|
function apiValidator (schema, payload/*, description*/) {
|
||||||
if (!schema) {
|
return new Promise(function Promise_apiValidator (resolve, reject) {
|
||||||
throw new errs.ValidationError("Schema is undefined");
|
if (typeof payload === 'undefined') {
|
||||||
}
|
reject(new error.ValidationError('Payload is undefined'));
|
||||||
|
|
||||||
// Can't use falsy check here as valid payload could be `0` or `false`
|
|
||||||
if (typeof payload === "undefined") {
|
|
||||||
throw new errs.ValidationError("Payload is undefined");
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = ajv.compile(schema);
|
|
||||||
|
|
||||||
const valid = validate(payload);
|
|
||||||
|
|
||||||
if (valid && !validate.errors) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep ajv's wording, which clients may match on, but not the regex it quotes: some are long
|
|
||||||
// and mean nothing to a user
|
|
||||||
for (const e of validate.errors || []) {
|
|
||||||
if (e.keyword === "pattern") {
|
|
||||||
e.message = "must match pattern";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const message = ajv.errorsText(validate.errors);
|
|
||||||
const err = new errs.ValidationError(message);
|
|
||||||
err.debug = { validationErrors: validate.errors, payload };
|
|
||||||
throw err;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apiValidator;
|
let validate = ajv.compile(schema);
|
||||||
|
let valid = validate(payload);
|
||||||
|
|
||||||
|
if (valid && !validate.errors) {
|
||||||
|
resolve(payload);
|
||||||
|
} else {
|
||||||
|
let message = ajv.errorsText(validate.errors);
|
||||||
|
let err = new error.ValidationError(message);
|
||||||
|
err.debug = [validate.errors, payload];
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
apiValidator.loadSchemas = parser
|
||||||
|
.dereference(path.resolve('schema/index.json'))
|
||||||
|
.then((schema) => {
|
||||||
|
ajv.addSchema(schema);
|
||||||
|
return schema;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = apiValidator;
|
||||||
@@ -1,48 +1,49 @@
|
|||||||
import net from "node:net";
|
const _ = require('lodash');
|
||||||
import Ajv from "ajv/dist/2020.js";
|
const error = require('../error');
|
||||||
import _ from "lodash";
|
const definitions = require('../../schema/definitions.json');
|
||||||
import commonDefinitions from "../../schema/common.json" with { type: "json" };
|
|
||||||
import errs from "../error.js";
|
|
||||||
|
|
||||||
RegExp.prototype.toJSON = RegExp.prototype.toString;
|
RegExp.prototype.toJSON = RegExp.prototype.toString;
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = require('ajv')({
|
||||||
verbose: true,
|
verbose: true, //process.env.NODE_ENV === 'development',
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
allowUnionTypes: true,
|
format: 'full', // strict regexes for format checks
|
||||||
coerceTypes: true,
|
coerceTypes: true,
|
||||||
strict: false,
|
schemas: [
|
||||||
schemas: [commonDefinitions],
|
definitions
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
ajv.addFormat("ipv6", { type: "string", validate: (value) => net.isIPv6(value) });
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Object} schema
|
* @param {Object} schema
|
||||||
* @param {Object} payload
|
* @param {Object} payload
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const validator = (schema, payload) => {
|
function validator (schema, payload) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(function (resolve, reject) {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
reject(new errs.InternalValidationError("Payload is falsy"));
|
reject(new error.InternalValidationError('Payload is falsy'));
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const validate = ajv.compile(schema);
|
let validate = ajv.compile(schema);
|
||||||
const valid = validate(payload);
|
|
||||||
|
|
||||||
|
let valid = validate(payload);
|
||||||
if (valid && !validate.errors) {
|
if (valid && !validate.errors) {
|
||||||
resolve(_.cloneDeep(payload));
|
resolve(_.cloneDeep(payload));
|
||||||
} else {
|
} else {
|
||||||
const message = ajv.errorsText(validate.errors);
|
let message = ajv.errorsText(validate.errors);
|
||||||
reject(new errs.InternalValidationError(message));
|
reject(new error.InternalValidationError(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default validator;
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = validator;
|
||||||
+11
-124
@@ -1,126 +1,13 @@
|
|||||||
import fs from "node:fs";
|
const {Signale} = require('signale');
|
||||||
import path from "node:path";
|
|
||||||
import signale from "signale";
|
|
||||||
import { isDebugMode } from "./lib/config.js";
|
|
||||||
|
|
||||||
const opts = {
|
module.exports = {
|
||||||
logLevel: "info",
|
global: new Signale({scope: 'Global '}),
|
||||||
|
migrate: new Signale({scope: 'Migrate '}),
|
||||||
|
express: new Signale({scope: 'Express '}),
|
||||||
|
access: new Signale({scope: 'Access '}),
|
||||||
|
nginx: new Signale({scope: 'Nginx '}),
|
||||||
|
ssl: new Signale({scope: 'SSL '}),
|
||||||
|
import: new Signale({scope: 'Importer '}),
|
||||||
|
setup: new Signale({scope: 'Setup '}),
|
||||||
|
ip_ranges: new Signale({scope: 'IP Ranges'})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Methods that are actually used across the codebase (see grep of `.info(`, `.warn(`, etc).
|
|
||||||
// Only these are mirrored to the log file - decorative signale methods (star, note, watch, ...)
|
|
||||||
// are left console-only since they carry no diagnostic value worth persisting.
|
|
||||||
const PERSISTED_METHODS = ["info", "warn", "error", "debug", "success", "fatal", "complete"];
|
|
||||||
|
|
||||||
const LOG_FILE = "/data/logs/backend.log";
|
|
||||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI colour codes before writing to disk
|
|
||||||
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
||||||
|
|
||||||
let fileStream = null;
|
|
||||||
let lastOpenAttempt = 0;
|
|
||||||
// If the file can't be opened (eg. running outside the standard Docker image, or in
|
|
||||||
// CI without /data), don't retry on every single log call - but do retry periodically
|
|
||||||
// so a transient issue (disk full, permissions fixed later) recovers without a restart.
|
|
||||||
const REOPEN_COOLDOWN_MS = 30 * 1000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazily opens the backend log file for appending. If the directory isn't writable,
|
|
||||||
* file logging is silently disabled and console logging continues unaffected.
|
|
||||||
*
|
|
||||||
* @returns {import('node:fs').WriteStream|null}
|
|
||||||
*/
|
|
||||||
const getFileStream = () => {
|
|
||||||
if (fileStream) {
|
|
||||||
return fileStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastOpenAttempt < REOPEN_COOLDOWN_MS) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
lastOpenAttempt = now;
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
||||||
const stream = fs.createWriteStream(LOG_FILE, { flags: "a" });
|
|
||||||
stream.on("error", () => {
|
|
||||||
fileStream = null;
|
|
||||||
});
|
|
||||||
fileStream = stream;
|
|
||||||
} catch (_err) {
|
|
||||||
fileStream = null;
|
|
||||||
}
|
|
||||||
return fileStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats and appends a single log line to the backend log file.
|
|
||||||
* Never throws - a failure here must never take down the application.
|
|
||||||
*
|
|
||||||
* @param {String} level
|
|
||||||
* @param {String} scope
|
|
||||||
* @param {Array} args
|
|
||||||
*/
|
|
||||||
const writeToFile = (level, scope, args) => {
|
|
||||||
const stream = getFileStream();
|
|
||||||
if (!stream) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = args
|
|
||||||
.map((arg) => {
|
|
||||||
if (typeof arg === "string") return arg;
|
|
||||||
if (arg instanceof Error) return arg.stack || arg.message;
|
|
||||||
try {
|
|
||||||
return JSON.stringify(arg);
|
|
||||||
} catch (_err) {
|
|
||||||
return String(arg);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join(" ")
|
|
||||||
.replace(ANSI_PATTERN, "");
|
|
||||||
|
|
||||||
const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(7)} [${scope.trim()}] ${message}\n`;
|
|
||||||
stream.write(line);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps a Signale instance so that every call to one of PERSISTED_METHODS is also
|
|
||||||
* appended to the backend log file, in addition to its normal console output.
|
|
||||||
*
|
|
||||||
* @param {Signale} instance
|
|
||||||
* @param {String} scope
|
|
||||||
* @returns {Signale}
|
|
||||||
*/
|
|
||||||
const withFileSink = (instance, scope) => {
|
|
||||||
for (const method of PERSISTED_METHODS) {
|
|
||||||
const original = instance[method].bind(instance);
|
|
||||||
instance[method] = (...args) => {
|
|
||||||
writeToFile(method, scope, args);
|
|
||||||
return original(...args);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createLogger = (scope) => withFileSink(new signale.Signale({ scope, ...opts }), scope);
|
|
||||||
|
|
||||||
const global = createLogger("Global ");
|
|
||||||
const migrate = createLogger("Migrate ");
|
|
||||||
const express = createLogger("Express ");
|
|
||||||
const access = createLogger("Access ");
|
|
||||||
const nginx = createLogger("Nginx ");
|
|
||||||
const ssl = createLogger("SSL ");
|
|
||||||
const certbot = createLogger("Certbot ");
|
|
||||||
const importer = createLogger("Importer ");
|
|
||||||
const setup = createLogger("Setup ");
|
|
||||||
const ipRanges = createLogger("IP Ranges");
|
|
||||||
const remoteVersion = createLogger("Remote Version");
|
|
||||||
|
|
||||||
const debug = (logger, ...args) => {
|
|
||||||
if (isDebugMode()) {
|
|
||||||
logger.debug(...args);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { debug, global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges, remoteVersion };
|
|
||||||
+13
-11
@@ -1,13 +1,15 @@
|
|||||||
import db from "./db.js";
|
const db = require('./db');
|
||||||
import { migrate as logger } from "./logger.js";
|
const logger = require('./logger').migrate;
|
||||||
|
|
||||||
const migrateUp = async () => {
|
module.exports = {
|
||||||
const version = await db().migrate.currentVersion();
|
latest: function () {
|
||||||
logger.info("Current database version:", version);
|
return db.migrate.currentVersion()
|
||||||
return await db().migrate.latest({
|
.then((version) => {
|
||||||
tableName: "migrations",
|
logger.info('Current database version:', version);
|
||||||
directory: "migrations",
|
return db.migrate.latest({
|
||||||
});
|
tableName: 'migrations',
|
||||||
|
directory: 'migrations'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { migrateUp };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'initial-schema';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "initial-schema";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,199 +7,199 @@ const migrateName = "initial-schema";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.createTable('auth', (table) => {
|
||||||
.createTable("auth", (table) => {
|
table.increments().primary();
|
||||||
table.increments().primary();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.integer('user_id').notNull().unsigned();
|
||||||
table.integer("user_id").notNull().unsigned();
|
table.string('type', 30).notNull();
|
||||||
table.string("type", 30).notNull();
|
table.string('secret').notNull();
|
||||||
table.string("secret").notNull();
|
table.json('meta').notNull();
|
||||||
table.json("meta").notNull();
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] auth Table created`);
|
logger.info('[' + migrate_name + '] auth Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("user", (table) => {
|
return knex.schema.createTable('user', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("is_disabled").notNull().unsigned().defaultTo(0);
|
table.integer('is_disabled').notNull().unsigned().defaultTo(0);
|
||||||
table.string("email").notNull();
|
table.string('email').notNull();
|
||||||
table.string("name").notNull();
|
table.string('name').notNull();
|
||||||
table.string("nickname").notNull();
|
table.string('nickname').notNull();
|
||||||
table.string("avatar").notNull();
|
table.string('avatar').notNull();
|
||||||
table.json("roles").notNull();
|
table.json('roles').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] user Table created`);
|
logger.info('[' + migrate_name + '] user Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("user_permission", (table) => {
|
return knex.schema.createTable('user_permission', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("user_id").notNull().unsigned();
|
table.integer('user_id').notNull().unsigned();
|
||||||
table.string("visibility").notNull();
|
table.string('visibility').notNull();
|
||||||
table.string("proxy_hosts").notNull();
|
table.string('proxy_hosts').notNull();
|
||||||
table.string("redirection_hosts").notNull();
|
table.string('redirection_hosts').notNull();
|
||||||
table.string("dead_hosts").notNull();
|
table.string('dead_hosts').notNull();
|
||||||
table.string("streams").notNull();
|
table.string('streams').notNull();
|
||||||
table.string("access_lists").notNull();
|
table.string('access_lists').notNull();
|
||||||
table.string("certificates").notNull();
|
table.string('certificates').notNull();
|
||||||
table.unique("user_id");
|
table.unique('user_id');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] user_permission Table created`);
|
logger.info('[' + migrate_name + '] user_permission Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("proxy_host", (table) => {
|
return knex.schema.createTable('proxy_host', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.json("domain_names").notNull();
|
table.json('domain_names').notNull();
|
||||||
table.string("forward_ip").notNull();
|
table.string('forward_ip').notNull();
|
||||||
table.integer("forward_port").notNull().unsigned();
|
table.integer('forward_port').notNull().unsigned();
|
||||||
table.integer("access_list_id").notNull().unsigned().defaultTo(0);
|
table.integer('access_list_id').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
|
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
|
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("caching_enabled").notNull().unsigned().defaultTo(0);
|
table.integer('caching_enabled').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
|
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
|
||||||
table.text("advanced_config").notNull().defaultTo("");
|
table.text('advanced_config').notNull().defaultTo('');
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table created`);
|
logger.info('[' + migrate_name + '] proxy_host Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("redirection_host", (table) => {
|
return knex.schema.createTable('redirection_host', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.json("domain_names").notNull();
|
table.json('domain_names').notNull();
|
||||||
table.string("forward_domain_name").notNull();
|
table.string('forward_domain_name').notNull();
|
||||||
table.integer("preserve_path").notNull().unsigned().defaultTo(0);
|
table.integer('preserve_path').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
|
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
|
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
|
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
|
||||||
table.text("advanced_config").notNull().defaultTo("");
|
table.text('advanced_config').notNull().defaultTo('');
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] redirection_host Table created`);
|
logger.info('[' + migrate_name + '] redirection_host Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("dead_host", (table) => {
|
return knex.schema.createTable('dead_host', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.json("domain_names").notNull();
|
table.json('domain_names').notNull();
|
||||||
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
|
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
|
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
|
||||||
table.text("advanced_config").notNull().defaultTo("");
|
table.text('advanced_config').notNull().defaultTo('');
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] dead_host Table created`);
|
logger.info('[' + migrate_name + '] dead_host Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("stream", (table) => {
|
return knex.schema.createTable('stream', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("incoming_port").notNull().unsigned();
|
table.integer('incoming_port').notNull().unsigned();
|
||||||
table.string("forward_ip").notNull();
|
table.string('forward_ip').notNull();
|
||||||
table.integer("forwarding_port").notNull().unsigned();
|
table.integer('forwarding_port').notNull().unsigned();
|
||||||
table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0);
|
table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0);
|
||||||
table.integer("udp_forwarding").notNull().unsigned().defaultTo(0);
|
table.integer('udp_forwarding').notNull().unsigned().defaultTo(0);
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] stream Table created`);
|
logger.info('[' + migrate_name + '] stream Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("access_list", (table) => {
|
return knex.schema.createTable('access_list', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.string("name").notNull();
|
table.string('name').notNull();
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list Table created`);
|
logger.info('[' + migrate_name + '] access_list Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("certificate", (table) => {
|
return knex.schema.createTable('certificate', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("owner_user_id").notNull().unsigned();
|
table.integer('owner_user_id').notNull().unsigned();
|
||||||
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
|
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
|
||||||
table.string("provider").notNull();
|
table.string('provider').notNull();
|
||||||
table.string("nice_name").notNull().defaultTo("");
|
table.string('nice_name').notNull().defaultTo('');
|
||||||
table.json("domain_names").notNull();
|
table.json('domain_names').notNull();
|
||||||
table.dateTime("expires_on").notNull();
|
table.dateTime('expires_on').notNull();
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] certificate Table created`);
|
logger.info('[' + migrate_name + '] certificate Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("access_list_auth", (table) => {
|
return knex.schema.createTable('access_list_auth', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("access_list_id").notNull().unsigned();
|
table.integer('access_list_id').notNull().unsigned();
|
||||||
table.string("username").notNull();
|
table.string('username').notNull();
|
||||||
table.string("password").notNull();
|
table.string('password').notNull();
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list_auth Table created`);
|
logger.info('[' + migrate_name + '] access_list_auth Table created');
|
||||||
|
|
||||||
return knex.schema.createTable("audit_log", (table) => {
|
return knex.schema.createTable('audit_log', (table) => {
|
||||||
table.increments().primary();
|
table.increments().primary();
|
||||||
table.dateTime("created_on").notNull();
|
table.dateTime('created_on').notNull();
|
||||||
table.dateTime("modified_on").notNull();
|
table.dateTime('modified_on').notNull();
|
||||||
table.integer("user_id").notNull().unsigned();
|
table.integer('user_id').notNull().unsigned();
|
||||||
table.string("object_type").notNull().defaultTo("");
|
table.string('object_type').notNull().defaultTo('');
|
||||||
table.integer("object_id").notNull().unsigned().defaultTo(0);
|
table.integer('object_id').notNull().unsigned().defaultTo(0);
|
||||||
table.string("action").notNull();
|
table.string('action').notNull();
|
||||||
table.json("meta").notNull();
|
table.json('meta').notNull();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] audit_log Table created`);
|
logger.info('[' + migrate_name + '] audit_log Table created');
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'websockets';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "websockets";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,29 +7,29 @@ const migrateName = "websockets";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0);
|
||||||
proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0);
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'forward_host';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "forward_host";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,17 @@ const migrateName = "forward_host";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.renameColumn('forward_ip', 'forward_host');
|
||||||
proxy_host.renameColumn("forward_ip", "forward_host");
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,11 +25,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'http2_support';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "http2_support";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,31 +7,31 @@ const migrateName = "http2_support";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0);
|
||||||
proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0);
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("redirection_host", (redirection_host) => {
|
return knex.schema.table('redirection_host', function (redirection_host) {
|
||||||
redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0);
|
redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("dead_host", (dead_host) => {
|
return knex.schema.table('dead_host', function (dead_host) {
|
||||||
dead_host.integer("http2_support").notNull().unsigned().defaultTo(0);
|
dead_host.integer('http2_support').notNull().unsigned().defaultTo(0);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] dead_host Table altered`);
|
logger.info('[' + migrate_name + '] dead_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,11 +39,11 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'forward_scheme';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "forward_scheme";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,17 @@ const migrateName = "forward_scheme";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.string('forward_scheme').notNull().defaultTo('http');
|
||||||
proxy_host.string("forward_scheme").notNull().defaultTo("http");
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,11 +25,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'disabled';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "disabled";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,38 +7,38 @@ const migrateName = "disabled";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.integer('enabled').notNull().unsigned().defaultTo(1);
|
||||||
proxy_host.integer("enabled").notNull().unsigned().defaultTo(1);
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("redirection_host", (redirection_host) => {
|
return knex.schema.table('redirection_host', function (redirection_host) {
|
||||||
redirection_host.integer("enabled").notNull().unsigned().defaultTo(1);
|
redirection_host.integer('enabled').notNull().unsigned().defaultTo(1);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("dead_host", (dead_host) => {
|
return knex.schema.table('dead_host', function (dead_host) {
|
||||||
dead_host.integer("enabled").notNull().unsigned().defaultTo(1);
|
dead_host.integer('enabled').notNull().unsigned().defaultTo(1);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] dead_host Table altered`);
|
logger.info('[' + migrate_name + '] dead_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("stream", (stream) => {
|
return knex.schema.table('stream', function (stream) {
|
||||||
stream.integer("enabled").notNull().unsigned().defaultTo(1);
|
stream.integer('enabled').notNull().unsigned().defaultTo(1);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] stream Table altered`);
|
logger.info('[' + migrate_name + '] stream Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,11 +46,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'custom_locations';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "custom_locations";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -9,17 +8,17 @@ const migrateName = "custom_locations";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.json('locations');
|
||||||
proxy_host.json("locations");
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,11 +26,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'hsts';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "hsts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,34 +7,34 @@ const migrateName = "hsts";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('proxy_host', function (proxy_host) {
|
||||||
.table("proxy_host", (proxy_host) => {
|
proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
|
||||||
proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
|
proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
|
||||||
proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
logger.info('[' + migrate_name + '] proxy_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("redirection_host", (redirection_host) => {
|
return knex.schema.table('redirection_host', function (redirection_host) {
|
||||||
redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
|
redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
|
||||||
redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
|
redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
|
|
||||||
return knex.schema.table("dead_host", (dead_host) => {
|
return knex.schema.table('dead_host', function (dead_host) {
|
||||||
dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
|
dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
|
||||||
dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
|
dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] dead_host Table altered`);
|
logger.info('[' + migrate_name + '] dead_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,11 +42,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'settings';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "settings";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,21 +7,21 @@ const migrateName = "settings";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.createTable('setting', (table) => {
|
||||||
.createTable("setting", (table) => {
|
table.string('id').notNull().primary();
|
||||||
table.string("id").notNull().primary();
|
table.string('name', 100).notNull();
|
||||||
table.string("name", 100).notNull();
|
table.string('description', 255).notNull();
|
||||||
table.string("description", 255).notNull();
|
table.string('value', 255).notNull();
|
||||||
table.string("value", 255).notNull();
|
table.json('meta').notNull();
|
||||||
table.json("meta").notNull();
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] setting Table created`);
|
logger.info('[' + migrate_name + '] setting Table created');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,11 +29,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'access_list_client';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "access_list_client";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,30 +7,32 @@ const migrateName = "access_list_client";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
.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(() => {
|
|
||||||
logger.info(`[${migrateName}] access_list_client Table created`);
|
|
||||||
|
|
||||||
return knex.schema.table("access_list", (access_list) => {
|
return knex.schema.createTable('access_list_client', (table) => {
|
||||||
access_list.integer("satify_any").notNull().defaultTo(0);
|
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(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list Table altered`);
|
logger.info('[' + migrate_name + '] access_list Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,14 +40,14 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (knex) => {
|
exports.down = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
return knex.schema.dropTable("access_list_client").then(() => {
|
return knex.schema.dropTable('access_list_client')
|
||||||
logger.info(`[${migrateName}] access_list_client Table dropped`);
|
.then(() => {
|
||||||
});
|
logger.info('[' + migrate_name + '] access_list_client Table dropped');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'access_list_client_fix';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "access_list_client_fix";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,17 @@ const migrateName = "access_list_client_fix";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('access_list', function (access_list) {
|
||||||
.table("access_list", (access_list) => {
|
access_list.renameColumn('satify_any', 'satisfy_any');
|
||||||
access_list.renameColumn("satify_any", "satisfy_any");
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list Table altered`);
|
logger.info('[' + migrate_name + '] access_list Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,11 +25,10 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (_knex) => {
|
exports.down = function (knex, Promise) {
|
||||||
logger.warn(`[${migrateName}] You can't migrate down this one.`);
|
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'pass_auth';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "pass_auth";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,18 @@ const migrateName = "pass_auth";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
.table("access_list", (access_list) => {
|
|
||||||
access_list.integer("pass_auth").notNull().defaultTo(1);
|
return knex.schema.table('access_list', function (access_list) {
|
||||||
})
|
access_list.integer('pass_auth').notNull().defaultTo(1);
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list Table altered`);
|
logger.info('[' + migrate_name + '] access_list Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,18 +26,16 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (knex) => {
|
exports.down = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('access_list', function (access_list) {
|
||||||
.table("access_list", (access_list) => {
|
access_list.dropColumn('pass_auth');
|
||||||
access_list.dropColumn("pass_auth");
|
})
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info(`[${migrateName}] access_list pass_auth Column dropped`);
|
logger.info('[' + migrate_name + '] access_list pass_auth Column dropped');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'redirection_scheme';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "redirection_scheme";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,18 @@ const migrateName = "redirection_scheme";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
.table("redirection_host", (table) => {
|
|
||||||
table.string("forward_scheme").notNull().defaultTo("$scheme");
|
return knex.schema.table('redirection_host', (table) => {
|
||||||
})
|
table.string('forward_scheme').notNull().defaultTo('$scheme');
|
||||||
.then(() => {
|
})
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
.then(function () {
|
||||||
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,18 +26,16 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (knex) => {
|
exports.down = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('redirection_host', (table) => {
|
||||||
.table("redirection_host", (table) => {
|
table.dropColumn('forward_scheme');
|
||||||
table.dropColumn("forward_scheme");
|
})
|
||||||
})
|
.then(function () {
|
||||||
.then(() => {
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'redirection_status_code';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "redirection_status_code";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
@@ -8,17 +7,18 @@ const migrateName = "redirection_status_code";
|
|||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const up = (knex) => {
|
exports.up = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
.table("redirection_host", (table) => {
|
|
||||||
table.integer("forward_http_code").notNull().unsigned().defaultTo(302);
|
return knex.schema.table('redirection_host', (table) => {
|
||||||
})
|
table.integer('forward_http_code').notNull().unsigned().defaultTo(302);
|
||||||
.then(() => {
|
})
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
.then(function () {
|
||||||
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,18 +26,16 @@ const up = (knex) => {
|
|||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
const down = (knex) => {
|
exports.down = function (knex/*, Promise*/) {
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('redirection_host', (table) => {
|
||||||
.table("redirection_host", (table) => {
|
table.dropColumn('forward_http_code');
|
||||||
table.dropColumn("forward_http_code");
|
})
|
||||||
})
|
.then(function () {
|
||||||
.then(() => {
|
logger.info('[' + migrate_name + '] redirection_host Table altered');
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,43 +1,40 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
const migrate_name = 'stream_domain';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
const migrateName = "stream_domain";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate
|
* Migrate
|
||||||
*
|
*
|
||||||
* @see http://knexjs.org/#Schema
|
* @see http://knexjs.org/#Schema
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
* @returns {Promise}
|
* @param {Promise} Promise
|
||||||
*/
|
* @returns {Promise}
|
||||||
const up = (knex) => {
|
*/
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
exports.up = function (knex/*, Promise*/) {
|
||||||
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('stream', (table) => {
|
||||||
.table("stream", (table) => {
|
table.renameColumn('forward_ip', 'forwarding_host');
|
||||||
table.renameColumn("forward_ip", "forwarding_host");
|
})
|
||||||
})
|
.then(function () {
|
||||||
.then(() => {
|
logger.info('[' + migrate_name + '] stream Table altered');
|
||||||
logger.info(`[${migrateName}] stream Table altered`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo Migrate
|
* Undo Migrate
|
||||||
*
|
*
|
||||||
* @param {Object} knex
|
* @param {Object} knex
|
||||||
* @returns {Promise}
|
* @param {Promise} Promise
|
||||||
*/
|
* @returns {Promise}
|
||||||
const down = (knex) => {
|
*/
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
exports.down = function (knex/*, Promise*/) {
|
||||||
|
logger.info('[' + migrate_name + '] Migrating Down...');
|
||||||
|
|
||||||
return knex.schema
|
return knex.schema.table('stream', (table) => {
|
||||||
.table("stream", (table) => {
|
table.renameColumn('forwarding_host', 'forward_ip');
|
||||||
table.renameColumn("forwarding_host", "forward_ip");
|
})
|
||||||
})
|
.then(function () {
|
||||||
.then(() => {
|
logger.info('[' + migrate_name + '] stream Table altered');
|
||||||
logger.info(`[${migrateName}] stream Table altered`);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
const migrate_name = 'ssl_passthrough_host';
|
||||||
|
const logger = require('../logger').migrate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate
|
||||||
|
*
|
||||||
|
* @see http://knexjs.org/#Schema
|
||||||
|
*
|
||||||
|
* @param {Object} knex
|
||||||
|
* @param {Promise} Promise
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
exports.up = async function (knex/*, Promise*/) {
|
||||||
|
logger.info('[' + migrate_name + '] Migrating Up...');
|
||||||
|
|
||||||
|
await knex.schema.createTable('ssl_passthrough_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.string('domain_name').notNull();
|
||||||
|
table.string('forwarding_host').notNull();
|
||||||
|
table.integer('forwarding_port').notNull().unsigned();
|
||||||
|
table.integer('enabled').notNull().unsigned().defaultTo(1);
|
||||||
|
table.json('meta').notNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('[' + migrate_name + '] Table created');
|
||||||
|
|
||||||
|
// Remove unique constraint so name can be used for new table
|
||||||
|
await knex.schema.alterTable('user_permission', (table) => {
|
||||||
|
table.dropUnique('user_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.renameTable('user_permission', 'user_permission_old');
|
||||||
|
|
||||||
|
// We need to recreate the table since sqlite does not support altering columns
|
||||||
|
await 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('ssl_passthrough_hosts').notNull();
|
||||||
|
table.string('access_lists').notNull();
|
||||||
|
table.string('certificates').notNull();
|
||||||
|
table.unique('user_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex('user_permission_old').select('*', 'streams as ssl_passthrough_hosts').then((data) => {
|
||||||
|
if (data.length) {
|
||||||
|
return knex('user_permission').insert(data);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists('user_permission_old');
|
||||||
|
|
||||||
|
logger.info('[' + migrate_name + '] permissions updated');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('stream').then(() => {
|
||||||
|
return knex.schema.table('user_permission', (table) => {
|
||||||
|
table.dropColumn('ssl_passthrough_hosts');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
logger.info('[' + migrate_name + '] Table altered and permissions updated');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import internalNginx from "../internal/nginx.js";
|
|
||||||
import { migrate as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const migrateName = "stream_domain";
|
|
||||||
|
|
||||||
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
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const up = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return regenerateDefaultHost(knex);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const down = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
|
||||||
|
|
||||||
return regenerateDefaultHost(knex);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const migrateName = "stream_ssl";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate
|
|
||||||
*
|
|
||||||
* @see http://knexjs.org/#Schema
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const up = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.table("stream", (table) => {
|
|
||||||
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] stream Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const down = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.table("stream", (table) => {
|
|
||||||
table.dropColumn("certificate_id");
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] stream Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const migrateName = "redirect_auto_scheme";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate
|
|
||||||
*
|
|
||||||
* @see http://knexjs.org/#Schema
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const up = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.table("redirection_host", async (table) => {
|
|
||||||
// change the column default from $scheme to auto
|
|
||||||
await table.string("forward_scheme").notNull().defaultTo("auto").alter();
|
|
||||||
await knex("redirection_host").where("forward_scheme", "$scheme").update({ forward_scheme: "auto" });
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const down = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.table("redirection_host", async (table) => {
|
|
||||||
await table.string("forward_scheme").notNull().defaultTo("$scheme").alter();
|
|
||||||
await knex("redirection_host").where("forward_scheme", "auto").update({ forward_scheme: "$scheme" });
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] redirection_host Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const migrateName = "trust_forwarded_proto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate
|
|
||||||
*
|
|
||||||
* @see http://knexjs.org/#Schema
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const up = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.alterTable("proxy_host", (table) => {
|
|
||||||
table.tinyint("trust_forwarded_proto").notNullable().defaultTo(0);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const down = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.alterTable("proxy_host", (table) => {
|
|
||||||
table.dropColumn("trust_forwarded_proto");
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] proxy_host Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { migrate as logger } from "../logger.js";
|
|
||||||
|
|
||||||
const migrateName = "user_permission_meta";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const up = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Up...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.alterTable("user_permission", (table) => {
|
|
||||||
table.json("meta").nullable();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] user_permission Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undo Migrate
|
|
||||||
*
|
|
||||||
* @param {Object} knex
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
const down = (knex) => {
|
|
||||||
logger.info(`[${migrateName}] Migrating Down...`);
|
|
||||||
|
|
||||||
return knex.schema
|
|
||||||
.alterTable("user_permission", (table) => {
|
|
||||||
table.dropColumn("meta");
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
logger.info(`[${migrateName}] user_permission Table altered`);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { up, down };
|
|
||||||
@@ -1,102 +1,102 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const User = require('./user');
|
||||||
import AccessListAuth from "./access_list_auth.js";
|
const AccessListAuth = require('./access_list_auth');
|
||||||
import AccessListClient from "./access_list_client.js";
|
const AccessListClient = require('./access_list_client');
|
||||||
import now from "./now_helper.js";
|
const now = require('./now_helper');
|
||||||
import ProxyHostModel from "./proxy_host.js";
|
|
||||||
import User from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
|
|
||||||
|
|
||||||
class AccessList extends Model {
|
class AccessList extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
static get name () {
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
return 'AccessList';
|
||||||
// Postgres returns COUNT() as a string
|
|
||||||
if (typeof thisJson.proxy_host_count === "string") {
|
|
||||||
thisJson.proxy_host_count = Number.parseInt(thisJson.proxy_host_count, 10);
|
|
||||||
}
|
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
static get tableName () {
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
return 'access_list';
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get jsonAttributes () {
|
||||||
return "AccessList";
|
return ['meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get relationMappings () {
|
||||||
return "access_list";
|
const ProxyHost = require('./proxy_host');
|
||||||
}
|
|
||||||
|
|
||||||
static get jsonAttributes() {
|
|
||||||
return ["meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
owner: {
|
owner: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "access_list.owner_user_id",
|
from: 'access_list.owner_user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("user.is_deleted", 0);
|
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('user.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
relation: Model.HasManyRelation,
|
relation: Model.HasManyRelation,
|
||||||
modelClass: AccessListAuth,
|
modelClass: AccessListAuth,
|
||||||
join: {
|
join: {
|
||||||
from: "access_list.id",
|
from: 'access_list.id',
|
||||||
to: "access_list_auth.access_list_id",
|
to: 'access_list_auth.access_list_id'
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clients: {
|
clients: {
|
||||||
relation: Model.HasManyRelation,
|
relation: Model.HasManyRelation,
|
||||||
modelClass: AccessListClient,
|
modelClass: AccessListClient,
|
||||||
join: {
|
join: {
|
||||||
from: "access_list.id",
|
from: 'access_list.id',
|
||||||
to: "access_list_client.access_list_id",
|
to: 'access_list_client.access_list_id'
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
proxy_hosts: {
|
proxy_hosts: {
|
||||||
relation: Model.HasManyRelation,
|
relation: Model.HasManyRelation,
|
||||||
modelClass: ProxyHostModel,
|
modelClass: ProxyHost,
|
||||||
join: {
|
join: {
|
||||||
from: "access_list.id",
|
from: 'access_list.id',
|
||||||
to: "proxy_host.access_list_id",
|
to: 'proxy_host.access_list_id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("proxy_host.is_deleted", 0);
|
qb.where('proxy_host.is_deleted', 0);
|
||||||
},
|
qb.omit(['is_deleted', 'meta']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get satisfy() {
|
||||||
|
return this.satisfy_any ? 'satisfy any' : 'satisfy all';
|
||||||
|
}
|
||||||
|
|
||||||
|
get passauth() {
|
||||||
|
return this.pass_auth ? '' : 'proxy_set_header Authorization "";';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AccessList;
|
module.exports = AccessList;
|
||||||
@@ -1,55 +1,55 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import accessListModel from "./access_list.js";
|
const now = require('./now_helper');
|
||||||
import now from "./now_helper.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
class AccessListAuth extends Model {
|
class AccessListAuth extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get name () {
|
||||||
return "AccessListAuth";
|
return 'AccessListAuth';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get tableName () {
|
||||||
return "access_list_auth";
|
return 'access_list_auth';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get jsonAttributes() {
|
static get jsonAttributes () {
|
||||||
return ["meta"];
|
return ['meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get relationMappings() {
|
static get relationMappings () {
|
||||||
return {
|
return {
|
||||||
access_list: {
|
access_list: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: accessListModel,
|
modelClass: require('./access_list'),
|
||||||
join: {
|
join: {
|
||||||
from: "access_list_auth.access_list_id",
|
from: 'access_list_auth.access_list_id',
|
||||||
to: "access_list.id",
|
to: 'access_list.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("access_list.is_deleted", 0);
|
qb.where('access_list.is_deleted', 0);
|
||||||
},
|
qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AccessListAuth;
|
module.exports = AccessListAuth;
|
||||||
@@ -1,55 +1,59 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import accessListModel from "./access_list.js";
|
const now = require('./now_helper');
|
||||||
import now from "./now_helper.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
class AccessListClient extends Model {
|
class AccessListClient extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get name () {
|
||||||
return "AccessListClient";
|
return 'AccessListClient';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get tableName () {
|
||||||
return "access_list_client";
|
return 'access_list_client';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get jsonAttributes() {
|
static get jsonAttributes () {
|
||||||
return ["meta"];
|
return ['meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get relationMappings() {
|
static get relationMappings () {
|
||||||
return {
|
return {
|
||||||
access_list: {
|
access_list: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: accessListModel,
|
modelClass: require('./access_list'),
|
||||||
join: {
|
join: {
|
||||||
from: "access_list_client.access_list_id",
|
from: 'access_list_client.access_list_id',
|
||||||
to: "access_list.id",
|
to: 'access_list.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("access_list.is_deleted", 0);
|
qb.where('access_list.is_deleted', 0);
|
||||||
},
|
qb.omit(['created_on', 'modified_on', 'is_deleted', 'access_list_id']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rule() {
|
||||||
|
return `${this.directive} ${this.address}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AccessListClient;
|
module.exports = AccessListClient;
|
||||||
+25
-22
@@ -1,52 +1,55 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import now from "./now_helper.js";
|
const User = require('./user');
|
||||||
import User from "./user.js";
|
const now = require('./now_helper');
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
class AuditLog extends Model {
|
class AuditLog extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get name () {
|
||||||
return "AuditLog";
|
return 'AuditLog';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get tableName () {
|
||||||
return "audit_log";
|
return 'audit_log';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get jsonAttributes() {
|
static get jsonAttributes () {
|
||||||
return ["meta"];
|
return ['meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get relationMappings() {
|
static get relationMappings () {
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "audit_log.user_id",
|
from: 'audit_log.user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
},
|
||||||
},
|
modify: function (qb) {
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'roles']);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuditLog;
|
module.exports = AuditLog;
|
||||||
+36
-60
@@ -1,110 +1,86 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import crypto from "node:crypto";
|
const bcrypt = require('bcrypt');
|
||||||
import bcrypt from "bcrypt";
|
const db = require('../db');
|
||||||
import { Model } from "objection";
|
const Model = require('objection').Model;
|
||||||
import db from "../db.js";
|
const User = require('./user');
|
||||||
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const now = require('./now_helper');
|
||||||
import now from "./now_helper.js";
|
|
||||||
import User from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = ["is_deleted"];
|
function encryptPassword () {
|
||||||
const saltRounds = 13;
|
/* jshint -W040 */
|
||||||
|
let _this = this;
|
||||||
|
|
||||||
// A hash of a random value, for a login that names an account with no password to check it against. Hashed
|
if (_this.type === 'password' && _this.secret) {
|
||||||
// with the same cost as real passwords, and made at startup so the first such login takes no longer either.
|
return bcrypt.hash(_this.secret, 13)
|
||||||
const dummyHash = bcrypt.hash(crypto.randomBytes(32).toString("hex"), saltRounds);
|
.then(function (hash) {
|
||||||
|
_this.secret = hash;
|
||||||
function encryptPassword() {
|
});
|
||||||
if (this.type === "password" && this.secret) {
|
|
||||||
return bcrypt.hash(this.secret, saltRounds).then((hash) => {
|
|
||||||
this.secret = hash;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Auth extends Model {
|
class Auth extends Model {
|
||||||
$beforeInsert(queryContext) {
|
$beforeInsert (queryContext) {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptPassword.apply(this, queryContext);
|
return encryptPassword.apply(this, queryContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate(queryContext) {
|
$beforeUpdate (queryContext) {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
return encryptPassword.apply(this, queryContext);
|
return encryptPassword.apply(this, queryContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a plain password against the encrypted password
|
* Verify a plain password against the encrypted password
|
||||||
*
|
*
|
||||||
* @param {String} password
|
* @param {String} password
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
verifyPassword(password) {
|
verifyPassword (password) {
|
||||||
return bcrypt.compare(password, this.secret);
|
return bcrypt.compare(password, this.secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static get name () {
|
||||||
* Take as long as verifyPassword does, then fail. bcrypt is nearly all of the time a login takes, so a login
|
return 'Auth';
|
||||||
* that skipped it for an unknown account would answer far sooner, and say which accounts exist.
|
|
||||||
*
|
|
||||||
* @param {String} password
|
|
||||||
* @returns {Promise<false>}
|
|
||||||
*/
|
|
||||||
static async verifyDummyPassword(password) {
|
|
||||||
await bcrypt.compare(password, await dummyHash);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get tableName () {
|
||||||
return "Auth";
|
return 'auth';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get jsonAttributes () {
|
||||||
return "auth";
|
return ['meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get jsonAttributes() {
|
static get relationMappings () {
|
||||||
return ["meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "auth.user_id",
|
from: 'auth.user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
is_deleted: 0,
|
is_deleted: 0
|
||||||
},
|
},
|
||||||
},
|
modify: function (qb) {
|
||||||
|
qb.omit(['is_deleted']);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Auth;
|
module.exports = Auth;
|
||||||
+35
-103
@@ -1,141 +1,73 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const User = require('./user');
|
||||||
import deadHostModel from "./dead_host.js";
|
const now = require('./now_helper');
|
||||||
import now from "./now_helper.js";
|
|
||||||
import proxyHostModel from "./proxy_host.js";
|
|
||||||
import redirectionHostModel from "./redirection_host.js";
|
|
||||||
import streamModel from "./stream.js";
|
|
||||||
import userModel from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = ["is_deleted"];
|
|
||||||
|
|
||||||
const cleanDomainNames = (domainNames) => {
|
|
||||||
// Sort domain_names
|
|
||||||
if (typeof domainNames !== "undefined") {
|
|
||||||
const newDomainNames = domainNames.filter((name) => name != null);
|
|
||||||
newDomainNames.sort();
|
|
||||||
return newDomainNames;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
class Certificate extends Model {
|
class Certificate extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for expires_on
|
// Default for expires_on
|
||||||
if (typeof this.expires_on === "undefined") {
|
if (typeof this.expires_on === 'undefined') {
|
||||||
this.expires_on = now();
|
this.expires_on = now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default for domain_names
|
// Default for domain_names
|
||||||
if (typeof this.domain_names !== "undefined") {
|
if (typeof this.domain_names === 'undefined') {
|
||||||
this.domain_names = cleanDomainNames(this.domain_names);
|
|
||||||
} else {
|
|
||||||
this.domain_names = [];
|
this.domain_names = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
if (typeof this.domain_names !== "undefined") {
|
|
||||||
this.domain_names = cleanDomainNames(this.domain_names);
|
// Sort domain_names
|
||||||
|
if (typeof this.domain_names !== 'undefined') {
|
||||||
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
static get name () {
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
return 'Certificate';
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
static get tableName () {
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
return 'certificate';
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get jsonAttributes () {
|
||||||
return "Certificate";
|
return ['domain_names', 'meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get relationMappings () {
|
||||||
return "certificate";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get jsonAttributes() {
|
|
||||||
return ["domain_names", "meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
owner: {
|
owner: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: userModel,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "certificate.owner_user_id",
|
from: 'certificate.owner_user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("user.is_deleted", 0);
|
qb.where('user.is_deleted', 0);
|
||||||
},
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
},
|
}
|
||||||
proxy_hosts: {
|
}
|
||||||
relation: Model.HasManyRelation,
|
|
||||||
modelClass: proxyHostModel,
|
|
||||||
join: {
|
|
||||||
from: "certificate.id",
|
|
||||||
to: "proxy_host.certificate_id",
|
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("proxy_host.is_deleted", 0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dead_hosts: {
|
|
||||||
relation: Model.HasManyRelation,
|
|
||||||
modelClass: deadHostModel,
|
|
||||||
join: {
|
|
||||||
from: "certificate.id",
|
|
||||||
to: "dead_host.certificate_id",
|
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("dead_host.is_deleted", 0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
redirection_hosts: {
|
|
||||||
relation: Model.HasManyRelation,
|
|
||||||
modelClass: redirectionHostModel,
|
|
||||||
join: {
|
|
||||||
from: "certificate.id",
|
|
||||||
to: "redirection_host.certificate_id",
|
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("redirection_host.is_deleted", 0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
streams: {
|
|
||||||
relation: Model.HasManyRelation,
|
|
||||||
modelClass: streamModel,
|
|
||||||
join: {
|
|
||||||
from: "certificate.id",
|
|
||||||
to: "stream.certificate_id",
|
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("stream.is_deleted", 0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Certificate;
|
module.exports = Certificate;
|
||||||
+37
-60
@@ -1,104 +1,81 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const User = require('./user');
|
||||||
import Certificate from "./certificate.js";
|
const Certificate = require('./certificate');
|
||||||
import now from "./now_helper.js";
|
const now = require('./now_helper');
|
||||||
import User from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
|
|
||||||
|
|
||||||
class DeadHost extends Model {
|
class DeadHost extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for domain_names
|
// Default for domain_names
|
||||||
if (typeof this.domain_names === "undefined") {
|
if (typeof this.domain_names === 'undefined') {
|
||||||
this.domain_names = [];
|
this.domain_names = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Sort domain_names
|
// Sort domain_names
|
||||||
if (typeof this.domain_names !== "undefined") {
|
if (typeof this.domain_names !== 'undefined') {
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
static get name () {
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
return 'DeadHost';
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
static get tableName () {
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
return 'dead_host';
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get jsonAttributes () {
|
||||||
return "DeadHost";
|
return ['domain_names', 'meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get relationMappings () {
|
||||||
return "dead_host";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get jsonAttributes() {
|
|
||||||
return ["domain_names", "meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultAllowGraph() {
|
|
||||||
return "[owner,certificate]";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultExpand() {
|
|
||||||
return ["certificate", "owner"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultOrder() {
|
|
||||||
return [castJsonIfNeed("domain_names"), "ASC"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
owner: {
|
owner: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "dead_host.owner_user_id",
|
from: 'dead_host.owner_user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("user.is_deleted", 0);
|
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('user.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
certificate: {
|
certificate: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: Certificate,
|
modelClass: Certificate,
|
||||||
join: {
|
join: {
|
||||||
from: "dead_host.certificate_id",
|
from: 'dead_host.certificate_id',
|
||||||
to: "certificate.id",
|
to: 'certificate.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("certificate.is_deleted", 0);
|
qb.where('certificate.is_deleted', 0);
|
||||||
},
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DeadHost;
|
module.exports = DeadHost;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const config = require('config');
|
||||||
import { isSqlite } from "../lib/config.js";
|
const Model = require('objection').Model;
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
export default () => {
|
module.exports = function () {
|
||||||
if (isSqlite()) {
|
if (config.database.knex && config.database.knex.client === 'sqlite3') {
|
||||||
return Model.raw("datetime('now','localtime')");
|
return Model.raw('datetime(\'now\',\'localtime\')');
|
||||||
|
} else {
|
||||||
|
return Model.raw('NOW()');
|
||||||
}
|
}
|
||||||
return Model.raw("NOW()");
|
|
||||||
};
|
};
|
||||||
@@ -1,146 +1,94 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const User = require('./user');
|
||||||
import AccessList from "./access_list.js";
|
const AccessList = require('./access_list');
|
||||||
import Certificate from "./certificate.js";
|
const Certificate = require('./certificate');
|
||||||
import now from "./now_helper.js";
|
const now = require('./now_helper');
|
||||||
import User from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = [
|
|
||||||
"is_deleted",
|
|
||||||
"ssl_forced",
|
|
||||||
"caching_enabled",
|
|
||||||
"block_exploits",
|
|
||||||
"allow_websocket_upgrade",
|
|
||||||
"http2_support",
|
|
||||||
"enabled",
|
|
||||||
"hsts_enabled",
|
|
||||||
"hsts_subdomains",
|
|
||||||
"trust_forwarded_proto",
|
|
||||||
];
|
|
||||||
|
|
||||||
class ProxyHost extends Model {
|
class ProxyHost extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for domain_names
|
// Default for domain_names
|
||||||
if (typeof this.domain_names === "undefined") {
|
if (typeof this.domain_names === 'undefined') {
|
||||||
this.domain_names = [];
|
this.domain_names = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Sort domain_names
|
// Sort domain_names
|
||||||
if (typeof this.domain_names !== "undefined") {
|
if (typeof this.domain_names !== 'undefined') {
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
static get name () {
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
return 'ProxyHost';
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
static get tableName () {
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
return 'proxy_host';
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get jsonAttributes () {
|
||||||
return "ProxyHost";
|
return ['domain_names', 'meta', 'locations'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get relationMappings () {
|
||||||
return "proxy_host";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get jsonAttributes() {
|
|
||||||
return ["domain_names", "meta", "locations"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultAllowGraph() {
|
|
||||||
return "[owner,access_list.[clients,items],certificate]";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultExpand() {
|
|
||||||
return ["owner", "certificate", "access_list.[clients,items]"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultOrder() {
|
|
||||||
return [castJsonIfNeed("domain_names"), "ASC"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
owner: {
|
owner: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "proxy_host.owner_user_id",
|
from: 'proxy_host.owner_user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("user.is_deleted", 0);
|
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('user.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
access_list: {
|
access_list: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: AccessList,
|
modelClass: AccessList,
|
||||||
join: {
|
join: {
|
||||||
from: "proxy_host.access_list_id",
|
from: 'proxy_host.access_list_id',
|
||||||
to: "access_list.id",
|
to: 'access_list.id'
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("access_list.is_deleted", 0);
|
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('access_list.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
certificate: {
|
certificate: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: Certificate,
|
modelClass: Certificate,
|
||||||
join: {
|
join: {
|
||||||
from: "proxy_host.certificate_id",
|
from: 'proxy_host.certificate_id',
|
||||||
to: "certificate.id",
|
to: 'certificate.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("certificate.is_deleted", 0);
|
qb.where('certificate.is_deleted', 0);
|
||||||
},
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
module.exports = ProxyHost;
|
||||||
* Restricts a proxy host query to hosts the user owns or has been granted by id
|
|
||||||
* (user permission meta.proxy_host_ids).
|
|
||||||
*
|
|
||||||
* @param {Object} query objection query on proxy_host
|
|
||||||
* @param {Number} userId
|
|
||||||
* @param {Object} [permissionMeta] the user's permission meta
|
|
||||||
* @returns {Object} the query
|
|
||||||
*/
|
|
||||||
export const scopeToOwnerOrAllowed = (query, userId, permissionMeta) => {
|
|
||||||
const allowedIds = permissionMeta?.proxy_host_ids || [];
|
|
||||||
return query.andWhere((builder) => {
|
|
||||||
builder.where("owner_user_id", userId);
|
|
||||||
if (allowedIds.length > 0) {
|
|
||||||
builder.orWhereIn("id", allowedIds);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProxyHost;
|
|
||||||
@@ -1,113 +1,81 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
|
const User = require('./user');
|
||||||
import Certificate from "./certificate.js";
|
const Certificate = require('./certificate');
|
||||||
import now from "./now_helper.js";
|
const now = require('./now_helper');
|
||||||
import User from "./user.js";
|
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
const boolFields = [
|
|
||||||
"is_deleted",
|
|
||||||
"enabled",
|
|
||||||
"preserve_path",
|
|
||||||
"ssl_forced",
|
|
||||||
"block_exploits",
|
|
||||||
"hsts_enabled",
|
|
||||||
"hsts_subdomains",
|
|
||||||
"http2_support",
|
|
||||||
];
|
|
||||||
|
|
||||||
class RedirectionHost extends Model {
|
class RedirectionHost extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
this.created_on = now();
|
this.created_on = now();
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Default for domain_names
|
// Default for domain_names
|
||||||
if (typeof this.domain_names === "undefined") {
|
if (typeof this.domain_names === 'undefined') {
|
||||||
this.domain_names = [];
|
this.domain_names = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
$beforeUpdate() {
|
$beforeUpdate () {
|
||||||
this.modified_on = now();
|
this.modified_on = now();
|
||||||
|
|
||||||
// Sort domain_names
|
// Sort domain_names
|
||||||
if (typeof this.domain_names !== "undefined") {
|
if (typeof this.domain_names !== 'undefined') {
|
||||||
this.domain_names.sort();
|
this.domain_names.sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$parseDatabaseJson(json) {
|
static get name () {
|
||||||
const thisJson = super.$parseDatabaseJson(json);
|
return 'RedirectionHost';
|
||||||
return convertIntFieldsToBool(thisJson, boolFields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$formatDatabaseJson(json) {
|
static get tableName () {
|
||||||
const thisJson = convertBoolFieldsToInt(json, boolFields);
|
return 'redirection_host';
|
||||||
return super.$formatDatabaseJson(thisJson);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get jsonAttributes () {
|
||||||
return "RedirectionHost";
|
return ['domain_names', 'meta'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get relationMappings () {
|
||||||
return "redirection_host";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get jsonAttributes() {
|
|
||||||
return ["domain_names", "meta"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultAllowGraph() {
|
|
||||||
return "[owner,certificate]";
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultExpand() {
|
|
||||||
return ["certificate", "owner"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get defaultOrder() {
|
|
||||||
return [castJsonIfNeed("domain_names"), "ASC"];
|
|
||||||
}
|
|
||||||
|
|
||||||
static get relationMappings() {
|
|
||||||
return {
|
return {
|
||||||
owner: {
|
owner: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
join: {
|
join: {
|
||||||
from: "redirection_host.owner_user_id",
|
from: 'redirection_host.owner_user_id',
|
||||||
to: "user.id",
|
to: 'user.id'
|
||||||
},
|
|
||||||
modify: (qb) => {
|
|
||||||
qb.where("user.is_deleted", 0);
|
|
||||||
},
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('user.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
certificate: {
|
certificate: {
|
||||||
relation: Model.HasOneRelation,
|
relation: Model.HasOneRelation,
|
||||||
modelClass: Certificate,
|
modelClass: Certificate,
|
||||||
join: {
|
join: {
|
||||||
from: "redirection_host.certificate_id",
|
from: 'redirection_host.certificate_id',
|
||||||
to: "certificate.id",
|
to: 'certificate.id'
|
||||||
},
|
},
|
||||||
modify: (qb) => {
|
modify: function (qb) {
|
||||||
qb.where("certificate.is_deleted", 0);
|
qb.where('certificate.is_deleted', 0);
|
||||||
},
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
|
||||||
},
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RedirectionHost;
|
module.exports = RedirectionHost;
|
||||||
+12
-12
@@ -1,30 +1,30 @@
|
|||||||
// Objection Docs:
|
// Objection Docs:
|
||||||
// http://vincit.github.io/objection.js/
|
// http://vincit.github.io/objection.js/
|
||||||
|
|
||||||
import { Model } from "objection";
|
const db = require('../db');
|
||||||
import db from "../db.js";
|
const Model = require('objection').Model;
|
||||||
|
|
||||||
Model.knex(db());
|
Model.knex(db);
|
||||||
|
|
||||||
class Setting extends Model {
|
class Setting extends Model {
|
||||||
$beforeInsert() {
|
$beforeInsert () {
|
||||||
// Default for meta
|
// Default for meta
|
||||||
if (typeof this.meta === "undefined") {
|
if (typeof this.meta === 'undefined') {
|
||||||
this.meta = {};
|
this.meta = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get name() {
|
static get name () {
|
||||||
return "Setting";
|
return 'Setting';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get tableName() {
|
static get tableName () {
|
||||||
return "setting";
|
return 'setting';
|
||||||
}
|
}
|
||||||
|
|
||||||
static get jsonAttributes() {
|
static get jsonAttributes () {
|
||||||
return ["meta"];
|
return ['meta'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Setting;
|
module.exports = Setting;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// 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 SslPassthrougHost 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 'SslPassthrougHost';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get tableName () {
|
||||||
|
return 'ssl_passthrough_host';
|
||||||
|
}
|
||||||
|
|
||||||
|
static get jsonAttributes () {
|
||||||
|
return ['meta'];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get relationMappings () {
|
||||||
|
return {
|
||||||
|
owner: {
|
||||||
|
relation: Model.HasOneRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'ssl_passthrough_host.owner_user_id',
|
||||||
|
to: 'user.id'
|
||||||
|
},
|
||||||
|
modify: function (qb) {
|
||||||
|
qb.where('user.is_deleted', 0);
|
||||||
|
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SslPassthrougHost;
|
||||||
Loaded 100 of 1126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user