Compare commits

..

7 Commits

Author SHA1 Message Date
Julian Reinhardt
70163a66fb Fixes migration 2021-10-25 13:08:35 +02:00
chaptergy
6e82161987 Adds comments to docker compose dev 2021-10-12 15:55:28 +02:00
chaptergy
f650137c84 Fixes eslint errors 2021-10-12 15:42:22 +02:00
chaptergy
02d3093d88 Finalizes SSL Passthrough hosts 2021-10-12 15:25:46 +02:00
chaptergy
ab026e5e18 Merge branch 'develop' 2021-10-12 13:44:50 +02:00
chaptergy
5a2548c89d WIP: complete control of new passthrough host type 2021-10-10 23:49:57 +02:00
chaptergy
5b1f0cead1 WIP: started adding new host type ssl passthrough 2021-10-10 23:49:07 +02:00
748 changed files with 45940 additions and 29565 deletions

View File

@@ -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@v9
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
stale-issue-message: 'Issue is now considered stale. If you want to keep it open, please comment :+1:'
stale-pr-message: 'PR is now considered stale. If you want to keep it open, please comment :+1:'
close-issue-message: 'Issue was closed due to inactivity.'
close-pr-message: 'PR was closed due to inactivity.'
days-before-stale: 182
days-before-close: 365
operations-per-run: 50

4
.gitignore vendored
View File

@@ -3,7 +3,3 @@
._* ._*
.vscode .vscode
certbot-help.txt certbot-help.txt
test/node_modules
*/node_modules
docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf

View File

@@ -1 +1 @@
2.12.6 2.9.9

347
Jenkinsfile vendored
View File

@@ -1,9 +1,3 @@
import groovy.transform.Field
@Field
def shOutput = ""
def buildxPushTags = ""
pipeline { pipeline {
agent { agent {
label 'docker-multiarch' label 'docker-multiarch'
@@ -14,12 +8,14 @@ pipeline {
ansiColor('xterm') ansiColor('xterm')
} }
environment { environment {
IMAGE = 'nginx-proxy-manager' IMAGE = "nginx-proxy-manager"
BUILD_VERSION = getVersion() BUILD_VERSION = getVersion()
MAJOR_VERSION = '2' MAJOR_VERSION = "2"
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('\\\\', '-').replaceAll('/', '-').replaceAll('\\.', '-')}" BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}"
BUILDX_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_FILE = 'docker/docker-compose.ci.yml'
COMPOSE_INTERACTIVE_NO_CLI = 1 COMPOSE_INTERACTIVE_NO_CLI = 1
BUILDX_NAME = "${COMPOSE_PROJECT_NAME}"
} }
stages { stages {
stage('Environment') { stage('Environment') {
@@ -30,7 +26,7 @@ pipeline {
} }
steps { steps {
script { script {
buildxPushTags = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest" env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:${BUILD_VERSION} -t docker.io/jc21/${IMAGE}:${MAJOR_VERSION} -t docker.io/jc21/${IMAGE}:latest"
} }
} }
} }
@@ -43,7 +39,7 @@ pipeline {
steps { steps {
script { script {
// Defaults to the Branch name, which is applies to all branches AND pr's // Defaults to the Branch name, which is applies to all branches AND pr's
buildxPushTags = "-t docker.io/nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}" env.BUILDX_PUSH_TAGS = "-t docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}"
} }
} }
} }
@@ -56,155 +52,109 @@ pipeline {
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
} }
} }
stage('Docker Login') {
steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh 'docker login -u "${duser}" -p "${dpass}"'
}
}
}
} }
} }
stage('Builds') { stage('Frontend') {
parallel {
stage('Project') {
steps {
script {
// Frontend and Backend
def shStatusCode = sh(label: 'Checking and Building', returnStatus: true, script: '''
set -e
./scripts/ci/frontend-build > ${WORKSPACE}/tmp-sh-build 2>&1
./scripts/ci/test-and-build > ${WORKSPACE}/tmp-sh-build 2>&1
''')
shOutput = readFile "${env.WORKSPACE}/tmp-sh-build"
if (shStatusCode != 0) {
error "${shOutput}"
}
}
}
post {
always {
sh 'rm -f ${WORKSPACE}/tmp-sh-build'
}
failure {
npmGithubPrComment("CI Error:\n\n```\n${shOutput}\n```", true)
}
}
}
stage('Docs') {
steps {
dir(path: 'docs') {
sh 'yarn install'
sh 'yarn build'
}
}
}
}
}
stage('Test Sqlite') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_sqlite"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.sqlite.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps { steps {
sh 'rm -rf ./test/results/junit/*' sh './scripts/frontend-build'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/sqlite'
sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/sqlite/docker_fullstack.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q stepca) > debug/sqlite/docker_stepca.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/sqlite/docker_pdns.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/sqlite/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/sqlite/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
} }
} }
stage('Test Mysql') { stage('Backend') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_mysql"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.mysql.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps { steps {
sh 'rm -rf ./test/results/junit/*' echo 'Checking Syntax ...'
sh './scripts/ci/fulltest-cypress' // See: https://github.com/yarnpkg/yarn/issues/3254
} sh '''docker run --rm \\
post { -v "$(pwd)/backend:/app" \\
always { -v "$(pwd)/global:/app/global" \\
// Dumps to analyze later -w /app \\
sh 'mkdir -p debug/mysql' node:latest \\
sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/mysql/docker_fullstack.log 2>&1' sh -c "yarn install && yarn eslint . && rm -rf node_modules"
sh 'docker logs $(docker-compose ps --all -q stepca) > debug/mysql/docker_stepca.log 2>&1' '''
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/mysql/docker_pdns.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/mysql/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/mysql/docker_dnsrouter.log 2>&1'
junit 'test/results/junit/*'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
}
}
stage('Test Postgres') {
environment {
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}_postgres"
COMPOSE_FILE = 'docker/docker-compose.ci.yml:docker/docker-compose.ci.postgres.yml'
}
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps {
sh 'rm -rf ./test/results/junit/*'
sh './scripts/ci/fulltest-cypress'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug/postgres'
sh 'docker logs $(docker-compose ps --all -q fullstack) > debug/postgres/docker_fullstack.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q stepca) > debug/postgres/docker_stepca.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns) > debug/postgres/docker_pdns.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q pdns-db) > debug/postgres/docker_pdns-db.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q dnsrouter) > debug/postgres/docker_dnsrouter.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q db-postgres) > debug/postgres/docker_db-postgres.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik) > debug/postgres/docker_authentik.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-redis) > debug/postgres/docker_authentik-redis.log 2>&1'
sh 'docker logs $(docker-compose ps --all -q authentik-ldap) > debug/postgres/docker_authentik-ldap.log 2>&1'
junit 'test/results/junit/*' echo 'Docker Build ...'
sh 'docker-compose down --remove-orphans --volumes -t 30 || true' sh '''docker build --pull --no-cache --squash --compress \\
} -t "${IMAGE}:ci-${BUILD_NUMBER}" \\
unstable { -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') { dir(path: 'test/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml') 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') { stage('MultiArch Build') {
when { when {
not { not {
@@ -212,64 +162,81 @@ pipeline {
} }
} }
steps { steps {
sh "./scripts/buildx --push ${buildxPushTags}" 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 / Comment') { stage('Docs Deploy') {
parallel { when {
stage('Docs Job') { allOf {
when { branch 'master'
allOf { not {
branch pattern: "^(develop|master)\$", comparator: "REGEXP" equals expected: 'UNSTABLE', actual: currentBuild.result
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
build wait: false, job: 'nginx-proxy-manager-docs', parameters: [string(name: 'docs_branch', value: "$BRANCH_NAME")]
} }
} }
stage('PR Comment') { }
when { steps {
allOf { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) {
changeRequest() sh """docker run --rm \\
not { --name \${COMPOSE_PROJECT_NAME}-docs-upload \\
equals expected: 'UNSTABLE', actual: currentBuild.result -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 \\
steps { -w /app \\
script { jc21/ci-tools \\
npmGithubPrComment("""Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/nginxproxymanager/${IMAGE}-dev): scripts/docs-upload /app/docs/.vuepress/dist/
``` """
nginxproxymanager/${IMAGE}-dev:${BRANCH_LOWER}
```
> [!NOTE] sh """docker run --rm \\
> Ensure you backup your NPM instance before testing this image! Especially if there are database changes. --name \${COMPOSE_PROJECT_NAME}-docs-invalidate \\
> This is a different docker image namespace than the official image. -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
> [!WARNING] jc21/ci-tools \\
> Changes and additions to DNS Providers require verification by at least 2 members of the community! aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*'
""", true) """
} }
}
}
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 { post {
always { always {
sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30'
sh 'echo Reverting ownership' sh 'echo Reverting ownership'
sh 'docker run --rm -v "$(pwd):/data" jc21/ci-tools chown -R "$(id -u):$(id -g)" /data' sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data'
printResult(true) }
success {
juxtapose event: 'success'
sh 'figlet "SUCCESS"'
} }
failure { failure {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
juxtapose event: 'failure'
sh 'figlet "FAILURE"'
} }
unstable { unstable {
archiveArtifacts(artifacts: 'debug/**/*.*', allowEmptyArchive: true) archiveArtifacts(artifacts: 'debug/**.*', allowEmptyArchive: true)
juxtapose event: 'unstable'
sh 'figlet "UNSTABLE"'
} }
} }
} }

451
README.md
View File

@@ -1,13 +1,22 @@
<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.12.6-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
@@ -19,7 +28,7 @@ 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 for entry here is low. so that the barrier for entry here is low.
@@ -56,29 +65,40 @@ I won't go in to too much detail here but here are the basics for someone new to
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
# If using docker-compose-plugin
docker compose up -d
``` ```
4. Log in to the Admin UI 4. Log in to the Admin UI
@@ -97,24 +117,397 @@ Password: changeme
Immediately after logging in with this default user you will be asked to modify your details and change your password. Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing ## Contributors
All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch. Special thanks to the following contributors:
CI is used in this project. All PR's must pass before being considered. After passing, <!-- prettier-ignore-start -->
docker builds for PR's are available on dockerhub for manual verifications. <!-- markdownlint-disable -->
<table>
Documentation within the `develop` branch is available for preview at <tr>
[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com) <td align="center">
<a href="https://github.com/Subv">
<img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80" alt=""/>
### Contributors <br /><sub><b>Sebastian Valle</b></sub>
</a>
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors). </td>
<td align="center">
<a href="https://github.com/Indemnity83">
## Getting Support <img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80" alt=""/>
<br /><sub><b>Kyle Klaus</b></sub>
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues) </a>
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions) </td>
3. [Reddit](https://reddit.com/r/nginxproxymanager) <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 -->

73
backend/.eslintrc.json Normal file
View File

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

11
backend/.prettierrc Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 320,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "all",
"proseWrap": "always"
}

8
backend/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

View File

@@ -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 { 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, 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) {
logger.debug(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;

View File

@@ -1,91 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.4/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": {
"recommended": true,
"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"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"database": { "database": {
"engine": "mysql2", "engine": "mysql",
"host": "db", "host": "db",
"name": "npm", "name": "npm",
"user": "npm", "user": "npm",

View File

@@ -1,32 +1,33 @@
import knex from "knex"; const config = require('config');
import {configGet, configHas} from "./lib/config.js";
const generateDbConfig = () => { if (!config.has('database')) {
if (!configHas("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');
throw new Error( }
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
);
}
const cfg = configGet("database"); function generateDbConfig() {
if (config.database.engine === 'knex-native') {
return config.database.knex;
} else
return {
client: config.database.engine,
connection: {
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.name,
port: config.database.port
},
migrations: {
tableName: 'migrations'
}
};
}
if (cfg.engine === "knex-native") {
return cfg.knex;
}
return { let data = generateDbConfig();
client: cfg.engine,
connection: {
host: cfg.host,
user: cfg.user,
password: cfg.password,
database: cfg.name,
port: cfg.port,
},
migrations: {
tableName: "migrations",
},
};
};
export default knex(generateDbConfig()); if (typeof config.database.version !== 'undefined') {
data.version = config.database.version;
}
module.exports = require('knex')(data);

1254
backend/doc/api.swagger.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +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();
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);
} }

View File

@@ -1,94 +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 utils from "../lib/utils.js"; const error = require('../lib/error');
import { access as logger } from "../logger.js"; const accessListModel = require('../models/access_list');
import accessListModel from "../models/access_list.js"; const accessListAuthModel = require('../models/access_list_auth');
import accessListAuthModel from "../models/access_list_auth.js"; const accessListClientModel = require('../models/access_list_client');
import accessListClientModel from "../models/access_list_client.js"; const proxyHostModel = require('../models/proxy_host');
import proxyHostModel from "../models/proxy_host.js"; const internalAuditLog = require('./audit-log');
import internalAuditLog from "./audit-log.js"; const internalNginx = require('./nginx');
import internalNginx from "./nginx.js"; const utils = require('../lib/utils');
const omissions = () => { function omissions () {
return ["is_deleted"]; return ['is_deleted'];
}; }
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; });
});
// Clients // Now add the clients
data.clients?.map((client) => { if (typeof data.clients !== 'undefined' && data.clients) {
promises.push( data.clients.map((client) => {
accessListClientModel.query().insert({ promises.push(accessListClientModel
access_list_id: row.id, .query()
address: client.address, .insert({
directive: client.directive, access_list_id: row.id,
}), address: client.address,
); directive: client.directive
return true; })
}); );
});
}
await Promise.all(promises); return Promise.all(promises);
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
// re-fetch with expansions return internalAccessList.build(row)
const freshRow = await internalAccessList.get( .then(() => {
access, if (row.proxy_host_count) {
{ return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
id: data.id, }
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"], })
}, .then(() => {
true // skip masking // Add to audit log
); return internalAuditLog.add(access, {
action: 'created',
// Audit log object_type: 'access-list',
data.meta = _.assign({}, data.meta || {}, freshRow.meta); object_id: row.id,
await internalAccessList.build(freshRow); meta: internalAccessList.maskItems(data)
});
if (Number.parseInt(freshRow.proxy_host_count, 10)) { })
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts); .then(() => {
} return internalAccessList.maskItems(row);
});
// 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);
}, },
/** /**
@@ -99,107 +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
const query = accessListAuthModel.query().delete().where("access_list_id", data.id); if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
if (itemsToKeep.length) { .query()
query.andWhere("username", "NOT IN", itemsToKeep); .where({id: data.id})
} .patch({
name: data.name,
await query; satisfy_any: data.satisfy_any,
// Add new items pass_auth: data.pass_auth,
if (promises.length) { });
await Promise.all(promises);
}
}
// Check for clients and add/update/remove them
if (typeof data.clients !== "undefined" && data.clients) {
const clientPromises = [];
data.clients.map((client) => {
if (client.address) {
clientPromises.push(
accessListClientModel.query().insert({
access_list_id: data.id,
address: client.address,
directive: client.directive,
}),
);
} }
return true; })
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
data.items.map(function (item) {
if (item.password) {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: data.id,
username: item.username,
password: item.password
})
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
});
let query = accessListAuthModel
.query()
.delete()
.where('access_list_id', data.id);
if (items_to_keep.length) {
query.andWhere('username', 'NOT IN', items_to_keep);
}
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let query = accessListClientModel
.query()
.delete()
.where('access_list_id', data.id);
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(internalNginx.reload)
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'access-list',
object_id: data.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
return internalAccessList.maskItems(row);
});
}); });
const query = accessListClientModel.query().delete().where("access_list_id", data.id);
await query;
// Add new clitens
if (clientPromises.length) {
await Promise.all(clientPromises);
}
}
// Add to audit log
await internalAuditLog.add(access, {
action: "updated",
object_type: "access-list",
object_id: data.id,
meta: internalAccessList.maskItems(data),
});
// re-fetch with expansions
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.[certificate,access_list.[clients,items]]"],
},
true // skip masking
);
await internalAccessList.build(freshRow)
if (Number.parseInt(row.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
await internalNginx.reload();
return internalAccessList.maskItems(row);
}, },
/** /**
@@ -208,50 +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( .select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
"proxy_host.is_deleted", .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)
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 || !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;
}, },
/** /**
@@ -261,64 +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 || !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 row.proxy_hosts.map(function (val, idx) {
.query() row.proxy_hosts[idx].access_list_id = 0;
.where("id", row.id) });
.patch({
is_deleted: 1,
});
// 2. update any proxy hosts that were using it (ignoring permissions) return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
if (row.proxy_hosts) { })
await proxyHostModel .then(() => {
.query() return internalNginx.reload();
.where("access_list_id", "=", row.id) });
.patch({ access_list_id: 0 }); }
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
// 3. reconfigure those hosts, then reload nginx try {
// set the access_list_id to zero for these items fs.unlinkSync(htpasswd_file);
row.proxy_hosts.map((_val, idx) => { } catch (err) {
row.proxy_hosts[idx].access_list_id = 0; // do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
});
})
.then(() => {
return true; return true;
}); });
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
}
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;
}, },
/** /**
@@ -326,73 +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 let query = accessListModel
.query() .query()
.count("id as count") .count('id as count')
.where("is_deleted", 0); .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);
});
}, },
/** /**
@@ -400,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;
}, },
@@ -424,7 +467,7 @@ const internalAccessList = {
* @returns {String} * @returns {String}
*/ */
getFilename: (list) => { getFilename: (list) => {
return `/data/access/${list.id}`; return '/data/access/' + list.id;
}, },
/** /**
@@ -434,55 +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() })
.each((_i, item, next) => { .catch((err) => {
if (item.password?.length) { logger.error(err);
logger.info(`Adding: ${item.username}`); next(err);
});
utils.execFile('openssl', ['passwd', '-apr1', item.password]) }
.then((res) => { })
try { .error((err) => {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'}); logger.error(err);
} catch (err) { reject(err);
reject(err); })
} .end((results) => {
next(); logger.success('Built Access file #' + list.id + ' for: ' + list.name);
}) resolve(results);
.catch((err) => { });
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`);
resolve(results);
}); });
}
}); });
}
} }
} };
export default internalAccessList; module.exports = internalAccessList;

View File

@@ -1,6 +1,5 @@
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 = {
@@ -9,31 +8,32 @@ const internalAuditLog = {
* *
* @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;
}, },
/** /**
@@ -50,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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,89 +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 = "";
}
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
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"],
});
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", freshRow);
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "dead-host",
object_id: freshRow.id,
meta: data,
});
return freshRow;
}, },
/** /**
@@ -92,79 +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}`,
);
}
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);
// 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
const newMeta = await internalNginx.configure(deadHostModel, "dead_host", row);
row.meta = newMeta;
return _.omit(internalHost.cleanRowCertificateMeta(thisRow), omissions());
}, },
/** /**
@@ -175,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("[owner,certificate]")
.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 || !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);
}
});
}, },
/** /**
@@ -210,30 +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 || !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 return deadHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
is_deleted: 1, is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
}); });
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "deleted",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
}, },
/** /**
@@ -243,39 +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 || !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 return deadHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 1, enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
}); });
// Configure nginx
await internalNginx.configure(deadHostModel, "dead_host", row);
// Add to audit log
await internalAuditLog.add(access, {
action: "enabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
}, },
/** /**
@@ -285,37 +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 || !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 return deadHostModel
.query() .query()
.where("id", row.id) .where('id', row.id)
.patch({ .patch({
enabled: 0, enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
}); });
// Delete Nginx Config
await internalNginx.deleteConfig("dead_host", row);
await internalNginx.reload();
// Add to audit log
await internalAuditLog.add(access, {
action: "disabled",
object_type: "dead-host",
object_id: row.id,
meta: _.omit(row, omissions()),
});
return true;
}, },
/** /**
@@ -323,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("[owner,certificate]") .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;
}, },
/** /**
@@ -364,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;

View File

@@ -1,10 +1,11 @@
import _ from "lodash"; const _ = require('lodash');
import { castJsonIfNeed } from "../lib/helpers.js"; const proxyHostModel = require('../models/proxy_host');
import deadHostModel from "../models/dead_host.js"; const redirectionHostModel = require('../models/redirection_host');
import proxyHostModel from "../models/proxy_host.js"; const deadHostModel = require('../models/dead_host');
import redirectionHostModel from "../models/redirection_host.js"; const sslPassthroughHostModel = require('../models/ssl_passthrough_host');
const internalHost = { const internalHost = {
/** /**
* Makes sure that the ssl_* and hsts_* fields play nicely together. * Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off. * ie: if there is no cert, then force_ssl is off.
@@ -14,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;
}, },
/** /**
@@ -39,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;
@@ -56,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 = {};
} }
@@ -65,33 +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)
];
const proxyRes = await proxyHostModel.query().where("is_deleted", 0); return Promise.all(promises)
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames); .then((promises_results) => {
responseObject.total_count += responseObject.proxy_hosts.length; let response_object = {
total_count: 0,
dead_hosts: [],
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;
});
}, },
/** /**
@@ -102,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;

View File

@@ -1,51 +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 errs from "../lib/error.js"; const internalNginx = require('./nginx');
import utils from "../lib/utils.js"; const { Liquid } = require('liquidjs');
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) => {
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, (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);
});
}); });
}, },
@@ -55,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;
}); });
} }
}) })
@@ -86,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;
}); });
} }
@@ -128,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;

View File

@@ -1,15 +1,14 @@
import fs from "node:fs"; const _ = require('lodash');
import { dirname } from "node:path"; const fs = require('fs');
import { fileURLToPath } from "node:url"; const logger = require('../logger').nginx;
import _ from "lodash"; const utils = require('../lib/utils');
import errs from "../lib/error.js"; const error = require('../lib/error');
import utils from "../lib/utils.js"; const { Liquid } = require('liquidjs');
import { nginx as logger } from "../logger.js"; const passthroughHostModel = require('../models/ssl_passthrough_host');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
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
@@ -25,65 +24,108 @@ 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, false, true);
}) })
.then(() => { .then(() => {
return internalNginx.generateConfig(host_type, host); if (host_type === 'ssl_passthrough_host' && !sslPassthroughEnabled){
// ssl passthrough is disabled
const meta = {
nginx_online: false,
nginx_err: 'SSL passthrough is not enabled in environment'
};
return passthroughHostModel
.query()
.where('is_deleted', 0)
.andWhere('enabled', 1)
.patch({
meta
}).then(() => {
return internalNginx.deleteConfig('ssl_passthrough_host', host, false);
});
} else {
return internalNginx.generateConfig(host_type, host);
}
}) })
.then(() => { .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;
}); });
logger.debug("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(() => {
internalNginx.renameConfigAsError(host_type, host);
}) })
.then(() => { .then(() => {
return internalNginx.deleteConfig(host_type, host, true); return internalNginx.deleteConfig(host_type, host, true);
@@ -102,18 +144,22 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
test: () => { test: () => {
logger.debug("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');
});
}, },
/** /**
@@ -122,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';
}, },
/** /**
@@ -134,49 +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 },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);
if (locationCopy.forward_host.indexOf("/") > -1) { if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split("/"); 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().then(() => resolve(renderedLocations)); locationRendering().then(() => resolve(renderedLocations));
}); });
}, },
@@ -185,74 +235,100 @@ const internalNginx = {
* @param {Object} host * @param {Object} host
* @returns {Promise} * @returns {Promise}
*/ */
generateConfig: (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);
logger.debug(`Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); if (debug_mode) {
logger.info('Generating ' + host_type + ' Config:', host);
}
const renderEngine = utils.getRenderEngine(); // logger.info('host = ' + JSON.stringify(host, null, 2));
return new Promise((resolve, reject) => { let renderEngine = new Liquid({
let template = null; root: __dirname + '/../templates/'
const filename = internalNginx.getConfigName(nice_host_type, host.id); });
try { let template = null;
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" }); let filename = internalNginx.getConfigName(host_type, host.id);
} catch (err) {
reject(new errs.ConfigurationError(err.message));
return;
}
let locationsPromise; try {
let origLocations; template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
} catch (err) {
throw new error.ConfigurationError(err.message);
}
// Manipulate the data a bit before sending it to the template let locationsPromise;
if (nice_host_type !== "default") { let origLocations;
host.use_default_location = true;
if (typeof host.advanced_config !== "undefined" && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
if (host.locations) { // Manipulate the data a bit before sending it to the template
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); if (host_type === 'ssl_passthrough_host') {
origLocations = [].concat(host.locations); if (internalNginx.sslPassthroughEnabled()){
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { const allHosts = await passthroughHostModel
host.locations = renderedLocations; .query()
}); .where('is_deleted', 0)
.groupBy('id')
// Allow someone who is using / custom location path to use it, and skip the default / location .omit(['is_deleted']);
_.map(host.locations, (location) => { host = {
if (location.path === "/") { all_passthrough_hosts: allHosts.map((host) => {
host.use_default_location = false; // Replace dots in domain
} host.forwarding_host = internalNginx.addIpv6Brackets(host.forwarding_host);
}); return host;
}),
};
} else { } else {
locationsPromise = Promise.resolve(); internalNginx.deleteConfig(host_type, host, false);
} }
// Set the IPv6 setting for the host } else if (host_type !== 'default') {
host.ipv6 = internalNginx.ipv6Enabled(); host.use_default_location = true;
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
locationsPromise.then(() => { if (host.locations) {
renderEngine //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
.parseAndRender(template, host) origLocations = [].concat(host.locations);
.then((config_text) => { locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" }); host.locations = renderedLocations;
logger.debug("Wrote config:", filename, config_text);
// Restore locations array
host.locations = origLocations;
resolve(true);
})
.catch((err) => {
logger.debug(`Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
});
}); });
// Allow someone who is using / custom location path to use it, and skip the default / location
_.map(host.locations, (location) => {
if (location.path === '/') {
host.use_default_location = false;
}
});
} else {
locationsPromise = Promise.resolve();
}
// Set the IPv6 setting for the host
host.ipv6 = internalNginx.ipv6Enabled();
return locationsPromise.then(() => {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (debug_mode) {
logger.success('Wrote config:', filename, config_text);
}
// Restore locations array
host.locations = origLocations;
return true;
})
.catch((err) => {
if (debug_mode) {
logger.warn('Could not write ' + filename + ':', err.message);
}
throw new error.ConfigurationError(err.message);
});
}); });
}, },
@@ -265,17 +341,22 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
generateLetsEncryptRequestConfig: (certificate) => { generateLetsEncryptRequestConfig: (certificate) => {
logger.debug("Generating LetsEncrypt Request Config:", certificate); if (debug_mode) {
const renderEngine = utils.getRenderEngine(); logger.info('Generating LetsEncrypt Request Config:', certificate);
}
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;
} }
@@ -284,72 +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'});
logger.debug("Wrote config:", filename, config_text);
if (debug_mode) {
logger.success('Wrote config:', filename, config_text);
}
resolve(true); resolve(true);
}) })
.catch((err) => { .catch((err) => {
logger.debug(`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) => {
logger.debug(`Deleting file: ${filename}`);
try {
fs.unlinkSync(filename);
} catch (err) {
logger.debug("Could not delete file:", JSON.stringify(err, null, 2));
}
},
/**
*
* @param {String} host_type
* @returns String
*/
getFileFriendlyHostType: (host_type) => {
return host_type.replace(/-/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();
}); });
}, },
@@ -357,23 +417,32 @@ 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, () => { try {
// ignore result, continue 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();
}); });
}, },
@@ -383,10 +452,9 @@ const internalNginx = {
* @returns {Promise} * @returns {Promise}
*/ */
bulkGenerateConfigs: (host_type, hosts) => { bulkGenerateConfigs: (host_type, hosts) => {
const promises = []; let promises = [];
hosts.map((host) => { hosts.map(function (host) {
promises.push(internalNginx.generateConfig(host_type, host)); promises.push(internalNginx.generateConfig(host_type, host));
return true;
}); });
return Promise.all(promises); return Promise.all(promises);
@@ -395,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);
@@ -411,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;

View File

@@ -1,106 +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 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);
}
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((row) => { .then((row) => {
// Configure nginx // Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => { return internalNginx.configure(proxyHostModel, 'proxy_host', row)
return 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;
}); });
@@ -114,110 +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 create_certificate = thisData.certificate_id === "new";
if (create_certificate) { 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}`,
);
} }
if (create_certificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, {
.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names,
domain_names: thisData.domain_names || row.domain_names, meta: _.assign({}, row.meta, data.meta)
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((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());
});
}); });
}); });
}, },
@@ -231,38 +213,41 @@ const internalProxyHost = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('proxy_hosts:get', data.id)
.can("proxy_hosts:get", thisData.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("[owner,access_list.[clients,items],certificate]") .allowEager('[owner,access_list,access_list.[clients,items],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) => {
if (!row || !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;
}); });
}, },
@@ -274,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 || !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())
}); });
}); });
}) })
@@ -319,41 +304,39 @@ 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"], expand: ['certificate', 'owner', 'access_list']
}); });
}) })
.then((row) => { .then((row) => {
if (!row || !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 proxyHostModel return proxyHostModel
.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(proxyHostModel, "proxy_host", row); return internalNginx.configure(proxyHostModel, 'proxy_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: "proxy-host", object_type: 'proxy-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -370,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 || !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())
}); });
}); });
}) })
@@ -420,38 +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("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") { // Query is used for searching
query.andWhere("owner_user_id", access.token.getUserId(1)); 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;
}, },
/** /**
@@ -462,16 +447,20 @@ const internalProxyHost = {
* @returns {Promise} * @returns {Promise}
*/ */
getCount: (user_id, visibility) => { 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') {
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 internalProxyHost; module.exports = internalProxyHost;

View File

@@ -1,105 +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 data.advanced_config === "undefined") { .omit(omissions())
data.advanced_config = ""; .insertAndFetch(data);
}
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;
}); });
@@ -113,107 +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}`,
);
} }
if (createCertificate) { if (create_certificate) {
return internalCertificate return internalCertificate.createQuickCertificate(access, {
.createQuickCertificate(access, { domain_names: data.domain_names || row.domain_names,
domain_names: thisData.domain_names || row.domain_names, meta: _.assign({}, row.meta, data.meta)
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());
}); });
}); });
}); });
@@ -228,39 +208,41 @@ const internalRedirectionHost = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('redirection_hosts:get', data.id)
.can("redirection_hosts:get", thisData.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("[owner,certificate]") .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 || !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;
}); });
}, },
@@ -272,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 || !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())
}); });
}); });
}) })
@@ -317,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 || !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())
}); });
}); });
}) })
@@ -368,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 || !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())
}); });
}); });
}) })
@@ -419,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("[owner,certificate]") .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);
} }
@@ -463,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;

View File

@@ -1,37 +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(userId, access_data.visibility), internalProxyHost.getCount(user_id, access_data.visibility),
internalRedirectionHost.getCount(userId, access_data.visibility), internalRedirectionHost.getCount(user_id, access_data.visibility),
internalStream.getCount(userId, access_data.visibility), internalStream.getCount(user_id, access_data.visibility),
internalDeadHost.getCount(userId, access_data.visibility), internalDeadHost.getCount(user_id, access_data.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;

View File

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

View File

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

View File

@@ -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,39 +103,40 @@ const internalStream = {
* @return {Promise} * @return {Promise}
*/ */
get: (access, data) => { get: (access, data) => {
const thisData = data || {}; if (typeof data === 'undefined') {
data = {};
}
return access return access.can('streams:get', data.id)
.can("streams:get", thisData.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("[owner,certificate]") .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 || !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;
}); });
}, },
@@ -221,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 || !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())
}); });
}); });
}) })
@@ -266,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 || !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())
}); });
}); });
}) })
@@ -317,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 || !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-host", object_type: 'stream-host',
object_id: row.id, object_id: row.id,
meta: _.omit(row, omissions()), meta: _.omit(row, omissions())
}); });
}); });
}) })
@@ -368,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("[owner,certificate]") .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;
}); });
}, },
@@ -412,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;

View File

@@ -1,14 +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');
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password"; module.exports = {
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
export default {
/** /**
* @param {Object} data * @param {Object} data
* @param {String} data.identity * @param {String} data.identity
@@ -18,66 +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) {
if (!user) { if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); // The scope requested doesn't exist as a role against the user,
} // you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
const auth = await authModel // Create a moment of the expiry expression
.query() let expiry = helpers.parseDatePeriod(data.expiry);
.where("user_id", "=", user.id) if (expiry === null) {
.where("type", "=", "password") throw new error.AuthError('Invalid expiry time: ' + data.expiry);
.first(); }
if (!auth) { return Token.create({
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH); iss: issuer || 'api',
} attrs: {
id: user.id
const valid = await auth.verifyPassword(data.secret); },
if (!valid) { scope: [data.scope],
throw new errs.AuthError( expiresIn: data.expiry
ERROR_MESSAGE_INVALID_AUTH, })
ERROR_MESSAGE_INVALID_AUTH_I18N, .then((signed) => {
); return {
} token: signed.token,
expires: expiry.toISOString()
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) { };
// The scope requested doesn't exist as a role against the user, });
// you shall not pass. } else {
throw new errs.AuthError(`Invalid scope: ${data.scope}`); throw new error.AuthError('Invalid password');
} }
});
// Create a moment of the expiry expression } else {
const expiry = parseDatePeriod(data.expiry); throw new error.AuthError('No password auth for user');
if (expiry === null) { }
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`); });
} } else {
throw new error.AuthError('No relevant user found');
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(),
};
}, },
/** /**
@@ -87,70 +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");
}, },
/** /**
* @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
}; };
}, });
}
}; };

View File

@@ -1,76 +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 userModel from "../models/user.js"; const gravatar = require('gravatar');
import userPermissionModel from "../models/user_permission.js"; const internalToken = require('./token');
import internalAuditLog from "./audit-log.js"; const internalAuditLog = require('./audit-log');
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;
}, },
/** /**
@@ -82,57 +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: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, })
})
.then(() => { .then(() => {
return user; return user;
}); });
@@ -148,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 || !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;
}); });
}, },
@@ -195,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;
});
}, },
/** /**
@@ -214,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())
}); });
}); });
}) })
@@ -250,14 +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
* *
@@ -266,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);
}); });
}, },
@@ -297,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);
}, },
/** /**
@@ -326,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;
@@ -345,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;
}); });
@@ -380,36 +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) => {
if (existing_auth) { if (existing_auth) {
// patch // patch
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({ return authModel
type: data.type, // This is required for the model to encrypt on save .query()
secret: data.secret, .where('user_id', user.id)
}); .andWhere('type', data.type)
.patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret
});
} else {
// insert
return authModel
.query()
.insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {}
});
} }
// insert
return authModel.query().insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
});
}) })
.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
}, }
}); });
}); });
}) })
@@ -424,17 +449,14 @@ 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 (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}`,
);
} }
return user; return user;
@@ -443,30 +465,34 @@ const internalUser = {
// 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((existing_auth) => { .then((existing_auth) => {
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 }, data)); .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 }, data));
}) })
.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(() => {
@@ -480,15 +506,14 @@ const internalUser = {
* @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;

View File

@@ -1,6 +1,6 @@
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',
@@ -9,7 +9,7 @@ module.exports = {
}, },
production: { production: {
client: 'mysql2', client: 'mysql',
migrations: { migrations: {
tableName: 'migrations', tableName: 'migrations',
stub: 'lib/migrate_template.js', stub: 'lib/migrate_template.js',

View File

@@ -4,90 +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 proxyHostModel from "../models/proxy_host.js"; const TokenModel = require('../models/token');
import TokenModel from "../models/token.js"; const roleSchema = require('./access/roles.json');
import userModel from "../models/user.js"; const permsSchema = require('./access/permissions.json');
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");
}
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;
}; };
/** /**
@@ -95,121 +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
query.andWhere("owner_user_id", tokenUserId); .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) => {
result.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, token: Token,
/** /**
* *
* @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*/) {
return await Token.load(tokenString); if (token_string) {
} resolve(Token.load(token_string));
allowInternalAccess = allowInternal; } else {
return allowInternal || null; allow_internal_access = allow_internal;
resolve(allow_internal_access || null);
}
});
}, },
reloadObjects: this.loadObjects, reloadObjects: this.loadObjects,
@@ -220,59 +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,
},
};
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 = ajv.validate("permissions", dataSchema);
return valid && dataSchema[permission];
} catch (err) {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw errs.PermissionError("Permission Denied", err);
}
},
}; };
} };

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "perms", "$id": "perms",
"definitions": { "definitions": {
"view": { "view": {

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "roles", "$id": "roles",
"definitions": { "definitions": {
"admin": { "admin": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,87 +0,0 @@
import batchflow from "batchflow";
import dnsPlugins from "../global/certbot-dns-plugins.json" with { type: "json" };
import { certbot as logger } from "../logger.js";
import errs from "./error.js";
import utils from "./utils.js";
const CERTBOT_VERSION_REPLACEMENT = "$(certbot --version | grep -Eo '[0-9](\\.[0-9]+)+')";
/**
* @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) => {
certbot
.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();
}
});
});
};
/**
* Installs a cerbot plugin given the key for the object from
* ../global/certbot-dns-plugins.json
*
* @param {string} pluginKey
* @returns {Object}
*/
const installPlugin = async (pluginKey) => {
if (typeof dnsPlugins[pluginKey] === "undefined") {
// throw Error(`Certbot plugin ${pluginKey} not found`);
throw new errs.ItemNotFoundError(pluginKey);
}
const plugin = dnsPlugins[pluginKey];
logger.start(`Installing ${pluginKey}...`);
plugin.version = plugin.version.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
plugin.dependencies = plugin.dependencies.replace(/{{certbot-version}}/g, CERTBOT_VERSION_REPLACEMENT);
// SETUPTOOLS_USE_DISTUTILS is required for certbot plugins to install correctly
// in new versions of Python
let env = Object.assign({}, process.env, { SETUPTOOLS_USE_DISTUTILS: "stdlib" });
if (typeof plugin.env === "object") {
env = Object.assign(env, plugin.env);
}
const cmd = `. /opt/certbot/bin/activate && pip install --no-cache-dir ${plugin.dependencies} ${plugin.package_name}${plugin.version} && deactivate`;
return utils
.exec(cmd, { env })
.then((result) => {
logger.complete(`Installed ${pluginKey}`);
return result;
})
.catch((err) => {
throw err;
});
};
export { installPlugins, installPlugin };

View File

@@ -1,244 +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 = '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}`);
instance = configData;
instance.keys = getKeys();
return;
}
}
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
if (envMysqlHost && envMysqlUser && envMysqlName) {
// we have enough mysql creds to go with mysql
logger.info("Using MySQL configuration");
instance = {
database: {
engine: mysqlEngine,
host: envMysqlHost,
port: process.env.DB_MYSQL_PORT || 3306,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
},
keys: getKeys(),
};
return;
}
const envPostgresHost = process.env.DB_POSTGRES_HOST || null;
const envPostgresUser = process.env.DB_POSTGRES_USER || null;
const envPostgresName = process.env.DB_POSTGRES_NAME || null;
if (envPostgresHost && envPostgresUser && envPostgresName) {
// we have enough postgres creds to go with postgres
logger.info("Using Postgres configuration");
instance = {
database: {
engine: postgresEngine,
host: envPostgresHost,
port: process.env.DB_POSTGRES_PORT || 5432,
user: envPostgresUser,
password: process.env.DB_POSTGRES_PASSWORD,
name: envPostgresName,
},
keys: getKeys(),
};
return;
}
const envSqliteFile = process.env.DB_SQLITE_FILE || "/data/database.sqlite";
logger.info(`Using Sqlite: ${envSqliteFile}`);
instance = {
database: {
engine: "knex-native",
knex: {
client: sqliteClientName,
connection: {
filename: envSqliteFile,
},
useNullAsDefault: true,
},
},
keys: getKeys(),
};
};
const getKeys = () => {
// Get keys from file
logger.debug("Cheecking 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));
} 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 && instance.database.knex.client === sqliteClientName;
};
/**
* 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 mdoe?
*
* @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 };

View File

@@ -1,103 +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;
}, },
AuthError: function (message, messageI18n, 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.message_i18n = messageI18n; this.public = true;
this.public = true; this.status = 401;
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;

View File

@@ -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", {
"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();
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,110 +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 { global as logger } from "../logger.js";
import errs from "./error.js";
const __filename = fileURLToPath(import.meta.url); module.exports = {
const __dirname = dirname(__filename);
const exec = async (cmd, options = {}) => {
logger.debug("CMD:", cmd);
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) => {
logger.debug(`CMD: ${cmd} ${args ? args.join(" ") : ""}`);
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 };

View File

@@ -1,12 +1,13 @@
import Ajv from "ajv/dist/2020.js"; const error = require('../error');
import errs from "../error.js"; const path = require('path');
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
}); });
/** /**
@@ -14,27 +15,31 @@ const ajv = new Ajv({
* @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` let validate = ajv.compile(schema);
if (typeof payload === "undefined") { let valid = validate(payload);
throw new errs.ValidationError("Payload is undefined");
}
const validate = ajv.compile(schema); if (valid && !validate.errors) {
const valid = validate(payload); resolve(payload);
} else {
let message = ajv.errorsText(validate.errors);
let err = new error.ValidationError(message);
err.debug = [validate.errors, payload];
reject(err);
}
});
}
if (valid && !validate.errors) { apiValidator.loadSchemas = parser
return payload; .dereference(path.resolve('schema/index.json'))
} .then((schema) => {
ajv.addSchema(schema);
return schema;
});
const message = ajv.errorsText(validate.errors); module.exports = apiValidator;
const err = new errs.ValidationError(message);
err.debug = [validate.errors, payload];
throw err;
};
export default apiValidator;

View File

@@ -1,17 +1,17 @@
import Ajv from 'ajv/dist/2020.js'; const _ = require('lodash');
import _ from "lodash"; const error = require('../error');
import commonDefinitions from "../../schema/common.json" with { type: "json" }; const definitions = require('../../schema/definitions.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
]
}); });
/** /**
@@ -20,26 +20,30 @@ const ajv = new Ajv({
* @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;

View File

@@ -1,18 +1,13 @@
import signale from "signale"; const {Signale} = require('signale');
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'})
}; };
const global = new signale.Signale({ scope: "Global ", ...opts });
const migrate = new signale.Signale({ scope: "Migrate ", ...opts });
const express = new signale.Signale({ scope: "Express ", ...opts });
const access = new signale.Signale({ scope: "Access ", ...opts });
const nginx = new signale.Signale({ scope: "Nginx ", ...opts });
const ssl = new signale.Signale({ scope: "SSL ", ...opts });
const certbot = new signale.Signale({ scope: "Certbot ", ...opts });
const importer = new signale.Signale({ scope: "Importer ", ...opts });
const setup = new signale.Signale({ scope: "Setup ", ...opts });
const ipRanges = new signale.Signale({ scope: "IP Ranges", ...opts });
export { global, migrate, express, access, nginx, ssl, certbot, importer, setup, ipRanges };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,10 +7,11 @@ 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.createTable('setting', (table) => { return knex.schema.createTable('setting', (table) => {
table.string('id').notNull().primary(); table.string('id').notNull().primary();
@@ -21,7 +21,7 @@ const up = (knex) => {
table.json('meta').notNull(); table.json('meta').notNull();
}) })
.then(() => { .then(() => {
logger.info(`[${migrateName}] setting Table created`); logger.info('[' + migrate_name + '] setting Table created');
}); });
}; };
@@ -29,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 };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,98 +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';
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;

View File

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

View File

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

View File

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

View File

@@ -1,92 +1,86 @@
// Objection Docs: // Objection Docs:
// http://vincit.github.io/objection.js/ // http://vincit.github.io/objection.js/
import bcrypt from "bcrypt"; const bcrypt = require('bcrypt');
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 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"]; function encryptPassword () {
/* jshint -W040 */
let _this = this;
function encryptPassword() { if (_this.type === 'password' && _this.secret) {
if (this.type === "password" && this.secret) { return bcrypt.hash(_this.secret, 13)
return bcrypt.hash(this.secret, 13).then((hash) => { .then(function (hash) {
this.secret = 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() { static get name () {
return "Auth"; return 'Auth';
} }
static get tableName() { static get tableName () {
return "auth"; return 'auth';
} }
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: "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;

View File

@@ -1,121 +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 userModel from "./user.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted"];
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 = []; 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 '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);
},
},
}; };
} }
} }
export default Certificate; module.exports = Certificate;

View File

@@ -1,92 +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 { 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 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;

View File

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

View File

@@ -1,114 +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 { 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",
];
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 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']);
}, }
}
}; };
} }
} }
export default ProxyHost; module.exports = ProxyHost;

View File

@@ -1,101 +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 { 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 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;

View File

@@ -1,8 +1,8 @@
// 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);
@@ -27,4 +27,4 @@ class Setting extends Model {
} }
} }
export default Setting; module.exports = Setting;

View File

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

View File

@@ -1,77 +1,56 @@
import { Model } from "objection"; // Objection Docs:
import db from "../db.js"; // http://vincit.github.io/objection.js/
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js"; const db = require('../db');
import now from "./now_helper.js"; const Model = require('objection').Model;
import User from "./user.js"; const User = require('./user');
const now = require('./now_helper');
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
class Stream extends Model { class Stream 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 'Stream';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'stream';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "Stream"; return ['meta'];
} }
static get tableName() { static get relationMappings () {
return "stream";
}
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: "stream.owner_user_id", from: 'stream.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']);
}, }
certificate: { }
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "stream.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
}; };
} }
} }
export default Stream; module.exports = Stream;

View File

@@ -3,44 +3,54 @@
and then has abilities after that. and then has abilities after that.
*/ */
import crypto from "node:crypto"; const _ = require('lodash');
import jwt from "jsonwebtoken"; const jwt = require('jsonwebtoken');
import _ from "lodash"; const crypto = require('crypto');
import { getPrivateKey, getPublicKey } from "../lib/config.js"; const error = require('../lib/error');
import errs from "../lib/error.js"; const ALGO = 'RS256';
import { global as logger } from "../logger.js";
const ALGO = "RS256"; let public_key = null;
let private_key = null;
export default () => { function checkJWTKeyPair() {
let tokenData = {}; if (!public_key || !private_key) {
let config = require('config');
public_key = config.get('jwt.pub');
private_key = config.get('jwt.key');
}
}
const self = { module.exports = function () {
let token_data = {};
let self = {
/** /**
* @param {Object} payload * @param {Object} payload
* @returns {Promise} * @returns {Promise}
*/ */
create: (payload) => { create: (payload) => {
if (!getPrivateKey()) {
logger.error("Private key is empty!");
}
// sign with RSA SHA256 // sign with RSA SHA256
const options = { let options = {
algorithm: ALGO, algorithm: ALGO,
expiresIn: payload.expiresIn || "1d", expiresIn: payload.expiresIn || '1d'
}; };
payload.jti = crypto.randomBytes(12).toString("base64").substring(-8); payload.jti = crypto.randomBytes(12)
.toString('base64')
.substr(-8);
checkJWTKeyPair();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
jwt.sign(payload, getPrivateKey(), options, (err, token) => { jwt.sign(payload, private_key, options, (err, token) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
tokenData = payload; token_data = payload;
resolve({ resolve({
token: token, token: token,
payload: payload, payload: payload
}); });
} }
}); });
@@ -51,47 +61,42 @@ export default () => {
* @param {String} token * @param {String} token
* @returns {Promise} * @returns {Promise}
*/ */
load: (token) => { load: function (token) {
if (!getPublicKey()) {
logger.error("Public key is empty!");
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
checkJWTKeyPair();
try { try {
if (!token || token === null || token === "null") { if (!token || token === null || token === 'null') {
reject(new errs.AuthError("Empty token")); reject(new error.AuthError('Empty token'));
} else { } else {
jwt.verify( jwt.verify(token, public_key, {ignoreExpiration: false, algorithms: [ALGO]}, (err, result) => {
token, if (err) {
getPublicKey(),
{ ignoreExpiration: false, algorithms: [ALGO] }, if (err.name === 'TokenExpiredError') {
(err, result) => { reject(new error.AuthError('Token has expired', err));
if (err) {
if (err.name === "TokenExpiredError") {
reject(new errs.AuthError("Token has expired", err));
} else {
reject(err);
}
} else { } else {
tokenData = result; reject(err);
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if (
typeof tokenData.scope !== "undefined" &&
_.indexOf(tokenData.scope, "all") !== -1
) {
tokenData.scope = ["user"];
}
resolve(tokenData);
} }
},
); } else {
token_data = result;
// Hack: some tokens out in the wild have a scope of 'all' instead of 'user'.
// For 30 days at least, we need to replace 'all' with user.
if ((typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, 'all') !== -1)) {
//console.log('Warning! Replacing "all" scope with "user"');
token_data.scope = ['user'];
}
resolve(token_data);
}
});
} }
} catch (err) { } catch (err) {
reject(err); reject(err);
} }
}); });
}, },
/** /**
@@ -100,15 +105,17 @@ export default () => {
* @param {String} scope * @param {String} scope
* @returns {Boolean} * @returns {Boolean}
*/ */
hasScope: (scope) => typeof tokenData.scope !== "undefined" && _.indexOf(tokenData.scope, scope) !== -1, hasScope: function (scope) {
return typeof token_data.scope !== 'undefined' && _.indexOf(token_data.scope, scope) !== -1;
},
/** /**
* @param {String} key * @param {String} key
* @return {*} * @return {*}
*/ */
get: (key) => { get: function (key) {
if (typeof tokenData[key] !== "undefined") { if (typeof token_data[key] !== 'undefined') {
return tokenData[key]; return token_data[key];
} }
return null; return null;
@@ -118,22 +125,22 @@ export default () => {
* @param {String} key * @param {String} key
* @param {*} value * @param {*} value
*/ */
set: (key, value) => { set: function (key, value) {
tokenData[key] = value; token_data[key] = value;
}, },
/** /**
* @param [defaultValue] * @param [default_value]
* @returns {Integer} * @returns {Integer}
*/ */
getUserId: (defaultValue) => { getUserId: (default_value) => {
const attrs = self.get("attrs"); let attrs = self.get('attrs');
if (attrs?.id) { if (attrs && typeof attrs.id !== 'undefined' && attrs.id) {
return attrs.id; return attrs.id;
} }
return defaultValue || 0; return default_value || 0;
}, }
}; };
return self; return self;

View File

@@ -1,65 +1,56 @@
// 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 UserPermission = require('./user_permission');
import now from "./now_helper.js"; const now = require('./now_helper');
import UserPermission from "./user_permission.js";
Model.knex(db); Model.knex(db);
const boolFields = ["is_deleted", "is_disabled"];
class User extends Model { class User extends Model {
$beforeInsert() { $beforeInsert () {
this.created_on = now(); this.created_on = now();
this.modified_on = now(); this.modified_on = now();
// Default for roles // Default for roles
if (typeof this.roles === "undefined") { if (typeof this.roles === 'undefined') {
this.roles = []; this.roles = [];
} }
} }
$beforeUpdate() { $beforeUpdate () {
this.modified_on = now(); this.modified_on = now();
} }
$parseDatabaseJson(json) { static get name () {
const thisJson = super.$parseDatabaseJson(json); return 'User';
return convertIntFieldsToBool(thisJson, boolFields);
} }
$formatDatabaseJson(json) { static get tableName () {
const thisJson = convertBoolFieldsToInt(json, boolFields); return 'user';
return super.$formatDatabaseJson(thisJson);
} }
static get name() { static get jsonAttributes () {
return "User"; return ['roles'];
} }
static get tableName() { static get relationMappings () {
return "user";
}
static get jsonAttributes() {
return ["roles"];
}
static get relationMappings() {
return { return {
permissions: { permissions: {
relation: Model.HasOneRelation, relation: Model.HasOneRelation,
modelClass: UserPermission, modelClass: UserPermission,
join: { join: {
from: "user.id", from: 'user.id',
to: "user_permission.user_id", to: 'user_permission.user_id'
}, },
}, modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'user_id']);
}
}
}; };
} }
} }
export default User; module.exports = User;

View File

@@ -1,9 +1,9 @@
// 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 now = require('./now_helper');
Model.knex(db); Model.knex(db);
@@ -26,4 +26,4 @@ class UserPermission extends Model {
} }
} }
export default UserPermission; module.exports = UserPermission;

View File

@@ -3,5 +3,5 @@
"ignore": [ "ignore": [
"data" "data"
], ],
"ext": "js json ejs cjs" "ext": "js json ejs"
} }

View File

@@ -1,49 +1,48 @@
{ {
"name": "nginx-proxy-manager", "name": "nginx-proxy-manager",
"version": "2.0.0", "version": "0.0.0",
"description": "A beautiful interface for creating Nginx endpoints", "description": "A beautiful interface for creating Nginx endpoints",
"author": "Jamie Curnow <jc@jc21.com>", "main": "js/index.js",
"license": "MIT",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js"
},
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.0", "ajv": "^6.12.0",
"ajv": "^8.17.1",
"archiver": "^5.3.0", "archiver": "^5.3.0",
"batchflow": "^0.4.0", "batchflow": "^0.4.0",
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"body-parser": "^1.20.3", "body-parser": "^1.19.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"express": "^4.20.0", "config": "^3.3.1",
"diskdb": "^0.1.17",
"express": "^4.17.1",
"express-fileupload": "^1.1.9", "express-fileupload": "^1.1.9",
"gravatar": "^1.8.0", "gravatar": "^1.8.0",
"jsonwebtoken": "^9.0.0", "html-entities": "^1.2.1",
"knex": "2.4.2", "json-schema-ref-parser": "^8.0.0",
"liquidjs": "10.6.1", "jsonwebtoken": "^8.5.1",
"knex": "^0.20.13",
"liquidjs": "^9.11.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.24.0",
"mysql2": "^3.11.1", "mysql": "^2.18.1",
"node-rsa": "^1.0.8", "node-rsa": "^1.0.8",
"objection": "3.0.1", "nodemon": "^2.0.2",
"objection": "^2.2.16",
"path": "^0.12.7", "path": "^0.12.7",
"pg": "^8.13.1", "pg": "^7.12.1",
"signale": "1.4.0", "restler": "^3.4.0",
"sqlite3": "5.1.6", "signale": "^1.4.0",
"temp-write": "^4.0.0" "sqlite3": "^4.1.1",
}, "temp-write": "^4.0.0",
"devDependencies": { "unix-timestamp": "^0.2.0"
"@apidevtools/swagger-parser": "^10.1.0",
"@biomejs/biome": "^2.2.4",
"chalk": "4.1.2",
"nodemon": "^2.0.2"
}, },
"signale": { "signale": {
"displayDate": true, "displayDate": true,
"displayTimestamp": true "displayTimestamp": true
},
"author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT",
"devDependencies": {
"eslint": "^6.8.0",
"eslint-plugin-align-assignments": "^1.1.2",
"prettier": "^2.0.4"
} }
} }

View File

@@ -0,0 +1,52 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalAuditLog = require('../../internal/audit-log');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/audit-log
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log
*
* Retrieve all logs
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAuditLog.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,60 @@
const express = require('express');
const pjson = require('../../package.json');
const error = require('../../lib/error');
const internalNginx = require('../../internal/nginx');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* Health Check
* GET /api
*/
router.get('/', (req, res/*, next*/) => {
let version = pjson.version.split('-').shift().split('.');
res.status(200).send({
status: 'OK',
version: {
major: parseInt(version.shift(), 10),
minor: parseInt(version.shift(), 10),
revision: parseInt(version.shift(), 10)
}
});
});
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));
router.use('/nginx/ssl-passthrough-hosts', require('./nginx/ssl_passthrough_hosts'));
router.use('/nginx/streams', require('./nginx/streams'));
router.use('/nginx/access-lists', require('./nginx/access_lists'));
router.use('/nginx/certificates', require('./nginx/certificates'));
router.get('/ssl-passthrough-enabled', (req, res/*, next*/) => {
res.status(200).send({
status: 'OK',
ssl_passthrough_enabled: internalNginx.sslPassthroughEnabled()
});
});
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, function (req, res, next) {
req.params.page = req.params['0'];
next(new error.ItemNotFoundError(req.params.page));
});
module.exports = router;

View File

@@ -0,0 +1,148 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalAccessList = require('../../../internal/access-list');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/access-lists
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists
*
* Retrieve all access-lists
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalAccessList.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/access-lists
*
* Create a new access-list
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/access-lists#/links/1/schema'}, req.body)
.then((payload) => {
return internalAccessList.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific access-list
*
* /api/nginx/access-lists/123
*/
router
.route('/:list_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists/123
*
* Retrieve a specific access-list
*/
.get((req, res, next) => {
validator({
required: ['list_id'],
additionalProperties: false,
properties: {
list_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
list_id: req.params.list_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalAccessList.get(res.locals.access, {
id: parseInt(data.list_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/access-lists/123
*
* Update and existing access-list
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/access-lists#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.list_id, 10);
return internalAccessList.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/access-lists/123
*
* Delete and existing access-list
*/
.delete((req, res, next) => {
internalAccessList.delete(res.locals.access, {id: parseInt(req.params.list_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,274 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalCertificate = require('../../../internal/certificate');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/certificates
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates
*
* Retrieve all certificates
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalCertificate.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/certificates
*
* Create a new certificate
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/certificates#/links/1/schema'}, req.body)
.then((payload) => {
req.setTimeout(900000); // 15 minutes timeout
return internalCertificate.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific certificate
*
* /api/nginx/certificates/123
*/
router
.route('/:certificate_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123
*
* Retrieve a specific certificate
*/
.get((req, res, next) => {
validator({
required: ['certificate_id'],
additionalProperties: false,
properties: {
certificate_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
certificate_id: req.params.certificate_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalCertificate.get(res.locals.access, {
id: parseInt(data.certificate_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/certificates/123
*
* Update and existing certificate
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/certificates#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.certificate_id, 10);
return internalCertificate.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/certificates/123
*
* Update and existing certificate
*/
.delete((req, res, next) => {
internalCertificate.delete(res.locals.access, {id: parseInt(req.params.certificate_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Upload Certs
*
* /api/nginx/certificates/123/upload
*/
router
.route('/:certificate_id/upload')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/upload
*
* Upload certificates
*/
.post((req, res, next) => {
if (!req.files) {
res.status(400)
.send({error: 'No files were uploaded'});
} else {
internalCertificate.upload(res.locals.access, {
id: parseInt(req.params.certificate_id, 10),
files: req.files
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
}
});
/**
* Renew LE Certs
*
* /api/nginx/certificates/123/renew
*/
router
.route('/:certificate_id/renew')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/123/renew
*
* Renew certificate
*/
.post((req, res, next) => {
req.setTimeout(900000); // 15 minutes timeout
internalCertificate.renew(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Download LE Certs
*
* /api/nginx/certificates/123/download
*/
router
.route('/:certificate_id/download')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/certificates/123/download
*
* Renew certificate
*/
.get((req, res, next) => {
internalCertificate.download(res.locals.access, {
id: parseInt(req.params.certificate_id, 10)
})
.then((result) => {
res.status(200)
.download(result.fileName);
})
.catch(next);
});
/**
* Validate Certs before saving
*
* /api/nginx/certificates/validate
*/
router
.route('/validate')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/certificates/validate
*
* Validate certificates
*/
.post((req, res, next) => {
if (!req.files) {
res.status(400)
.send({error: 'No files were uploaded'});
} else {
internalCertificate.validate({
files: req.files
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
}
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalDeadHost = require('../../../internal/dead-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/dead-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts
*
* Retrieve all dead-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalDeadHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/dead-hosts
*
* Create a new dead-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/dead-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalDeadHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific dead-host
*
* /api/nginx/dead-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/dead-hosts/123
*
* Retrieve a specific dead-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalDeadHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/dead-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalDeadHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/dead-hosts/123
*
* Update and existing dead-host
*/
.delete((req, res, next) => {
internalDeadHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable dead-host
*
* /api/nginx/dead-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/enable
*/
.post((req, res, next) => {
internalDeadHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable dead-host
*
* /api/nginx/dead-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/dead-hosts/123/disable
*/
.post((req, res, next) => {
internalDeadHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalProxyHost = require('../../../internal/proxy-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/proxy-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts
*
* Retrieve all proxy-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalProxyHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/proxy-hosts
*
* Create a new proxy-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/proxy-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalProxyHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific proxy-host
*
* /api/nginx/proxy-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/proxy-hosts/123
*
* Retrieve a specific proxy-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalProxyHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/proxy-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalProxyHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/proxy-hosts/123
*
* Update and existing proxy-host
*/
.delete((req, res, next) => {
internalProxyHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable proxy-host
*
* /api/nginx/proxy-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/enable
*/
.post((req, res, next) => {
internalProxyHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable proxy-host
*
* /api/nginx/proxy-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/proxy-hosts/123/disable
*/
.post((req, res, next) => {
internalProxyHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalRedirectionHost = require('../../../internal/redirection-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/redirection-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts
*
* Retrieve all redirection-hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalRedirectionHost.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/redirection-hosts
*
* Create a new redirection-host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/redirection-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalRedirectionHost.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific redirection-host
*
* /api/nginx/redirection-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/redirection-hosts/123
*
* Retrieve a specific redirection-host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalRedirectionHost.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/redirection-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalRedirectionHost.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/redirection-hosts/123
*
* Update and existing redirection-host
*/
.delete((req, res, next) => {
internalRedirectionHost.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable redirection-host
*
* /api/nginx/redirection-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/enable
*/
.post((req, res, next) => {
internalRedirectionHost.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable redirection-host
*
* /api/nginx/redirection-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/redirection-hosts/123/disable
*/
.post((req, res, next) => {
internalRedirectionHost.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalSslPassthrough = require('../../../internal/ssl-passthrough-host');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/ssl-passthrough-hosts
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/ssl-passthrough-hosts
*
* Retrieve all ssl passthrough hosts
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalSslPassthrough.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/ssl-passthrough-hosts
*
* Create a new ssl passthrough host
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/ssl-passthrough-hosts#/links/1/schema'}, req.body)
.then((payload) => {
return internalSslPassthrough.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific ssl passthrough host
*
* /api/nginx/ssl-passthrough-hosts/123
*/
router
.route('/:host_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/ssl-passthrough-hosts/123
*
* Retrieve a specific ssl passthrough host
*/
.get((req, res, next) => {
validator({
required: ['host_id'],
additionalProperties: false,
properties: {
host_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
host_id: req.params.host_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalSslPassthrough.get(res.locals.access, {
id: parseInt(data.host_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/ssl-passthrough-hosts/123
*
* Update an existing ssl passthrough host
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/ssl-passthrough-hosts#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.host_id, 10);
return internalSslPassthrough.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/ssl-passthrough-hosts/123
*
* Delete an ssl passthrough host
*/
.delete((req, res, next) => {
internalSslPassthrough.delete(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable ssl passthrough host
*
* /api/nginx/ssl-passthrough-hosts/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/ssl-passthrough-hosts/123/enable
*/
.post((req, res, next) => {
internalSslPassthrough.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable ssl passthrough host
*
* /api/nginx/ssl-passthrough-hosts/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/ssl-passthrough-hosts/123/disable
*/
.post((req, res, next) => {
internalSslPassthrough.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,196 @@
const express = require('express');
const validator = require('../../../lib/validator');
const jwtdecode = require('../../../lib/express/jwt-decode');
const internalStream = require('../../../internal/stream');
const apiValidator = require('../../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/nginx/streams
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams
*
* Retrieve all streams
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalStream.getAll(res.locals.access, data.expand, data.query);
})
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
})
/**
* POST /api/nginx/streams
*
* Create a new stream
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/streams#/links/1/schema'}, req.body)
.then((payload) => {
return internalStream.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific stream
*
* /api/nginx/streams/123
*/
router
.route('/:stream_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes
/**
* GET /api/nginx/streams/123
*
* Retrieve a specific stream
*/
.get((req, res, next) => {
validator({
required: ['stream_id'],
additionalProperties: false,
properties: {
stream_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
stream_id: req.params.stream_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalStream.get(res.locals.access, {
id: parseInt(data.stream_id, 10),
expand: data.expand
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/nginx/streams/123
*
* Update and existing stream
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/streams#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = parseInt(req.params.stream_id, 10);
return internalStream.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/nginx/streams/123
*
* Update and existing stream
*/
.delete((req, res, next) => {
internalStream.delete(res.locals.access, {id: parseInt(req.params.stream_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Enable stream
*
* /api/nginx/streams/123/enable
*/
router
.route('/:host_id/enable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/enable
*/
.post((req, res, next) => {
internalStream.enable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Disable stream
*
* /api/nginx/streams/123/disable
*/
router
.route('/:host_id/disable')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/nginx/streams/123/disable
*/
.post((req, res, next) => {
internalStream.disable(res.locals.access, {id: parseInt(req.params.host_id, 10)})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,29 @@
const express = require('express');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalReport = require('../../internal/report');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/hosts')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /reports/hosts
*/
.get(jwtdecode(), (req, res, next) => {
internalReport.getHostsReport(res.locals.access)
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,36 @@
const express = require('express');
const swaggerJSON = require('../../doc/api.swagger.json');
const PACKAGE = require('../../package.json');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /schema
*/
.get((req, res/*, next*/) => {
let proto = req.protocol;
if (typeof req.headers['x-forwarded-proto'] !== 'undefined' && req.headers['x-forwarded-proto']) {
proto = req.headers['x-forwarded-proto'];
}
let origin = proto + '://' + req.hostname;
if (typeof req.headers.origin !== 'undefined' && req.headers.origin) {
origin = req.headers.origin;
}
swaggerJSON.info.version = PACKAGE.version;
swaggerJSON.servers[0].url = origin + '/api';
res.status(200).send(swaggerJSON);
});
module.exports = router;

Some files were not shown because too many files have changed in this diff Show More