Compare commits

..

4 Commits

Author SHA1 Message Date
jc21
066a765a4d Merge pull request #1048 from jc21/develop
static-content <- develop
2021-04-29 21:36:38 +10:00
Nikolaj Frey
5a3d32db7b Frontend form and marionette modifications to allow static hosts and locations 2021-02-08 12:49:49 +10:00
Nikolaj Frey
8de118d875 Backend schema and migration modifications for allowing static hosts and locations 2021-02-08 12:47:00 +10:00
Nikolaj Frey
f61ab55b52 Added a restart dev script which destroys then starts dev 2021-02-08 12:47:00 +10:00
724 changed files with 43536 additions and 30047 deletions

View File

@@ -6,30 +6,20 @@ labels: bug
assignees: ''
---
<!--
Are you in the right place?
**Are you in the right place?**
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*
-->
**Checklist**
- Have you pulled and found the error with `jc21/nginx-proxy-manager:latest` docker image?
- Yes / No
- Are you sure you're not using someone else's docker image?
- Yes / No
- Have you searched for similar issues (both open and closed)?
- Yes / No
- If having problems with Lets Encrypt, have you made absolutely sure your site is accessible from outside of your network?
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Nginx Proxy Manager Version**
<!-- What version of Nginx Proxy Manager is reported on the login page? -->
- A clear and concise description of what the bug is.
- What version of Nginx Proxy Manager is reported on the login page?
**To Reproduce**
Steps to reproduce the behavior:
@@ -38,18 +28,14 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
A clear and concise description of what you expected to happen.
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
If applicable, add screenshots to help explain your problem.
**Operating System**
<!-- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error. -->
- Please specify if using a Rpi, Mac, orchestration tool or any other setups that might affect the reproduction of this error.
**Additional context**
<!-- Add any other context about the problem here, docker version, browser version, logs if applicable to the problem. Too much info is better than too little. -->
Add any other context about the problem here, docker version, browser version if applicable to the problem. Too much info is better than too little.

View File

@@ -1,18 +0,0 @@
---
name: DNS challenge provider request
about: Suggest a new provider to be available for a certificate DNS challenge
title: ''
labels: dns provider request
assignees: ''
---
**What provider would you like to see added to NPM?**
<!-- What is this provider called? -->
**Have you checked if a certbot plugin exists?**
<!--
Currently NPM only supports DNS challenge providers for which a certbot plugin exists.
You can visit pypi.org, and search for a package with the name `certbot-dns-<privider>`.
-->

View File

@@ -7,26 +7,19 @@ assignees: ''
---
<!--
Are you in the right place?
**Are you in the right place?**
- If you are looking for support on how to get your upstream server forwarding, please consider asking the community on Reddit.
- If you are writing code changes to contribute and need to ask about the internals of the software, Gitter is the best place to ask.
- If you think you found a bug with NPM (not Nginx, or your upstream server or MySql) then you are in the *right place.*
-->
- If you have a feature request for NPM then you are in the *right place.*
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
Add any other context or screenshots about the feature request here.

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
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.8.1

347
Jenkinsfile vendored
View File

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

285
README.md
View File

@@ -1,25 +1,31 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<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.8.1-green.svg?style=for-the-badge">
<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">
</a>
<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">
</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>
</p>
This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup)
- [Quick Setup](https://nginxproxymanager.com#quick-setup)
- [Full Setup](https://nginxproxymanager.com/setup/)
- [Screenshots](https://nginxproxymanager.com/screenshots/)
## 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.
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.
@@ -46,75 +52,210 @@ I won't go in to too much detail here but here are the basics for someone new to
3. Configure your domain name details to point to your home, either with a static ip or a service like DuckDNS or [Amazon Route53](https://github.com/jc21/route53-ddns)
4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. Install Docker and Docker-Compose
## Contributors
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
Special thanks to the following contributors:
2. Create a docker-compose.yml file similar to this:
```yml
services:
app:
image: 'docker.io/jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
```
This is the bare minimum configuration required. See the [documentation](https://nginxproxymanager.com/setup/) for more.
3. Bring up your stack by running
```bash
docker-compose up -d
# If using docker-compose-plugin
docker compose up -d
```
4. Log in to the Admin UI
When your docker container is running, connect to it on port `81` for the admin interface.
Sometimes this can take a little bit because of the entropy of keys.
[http://127.0.0.1:81](http://127.0.0.1:81)
Default Admin User:
```
Email: admin@example.com
Password: changeme
```
Immediately after logging in with this default user you will be asked to modify your details and change your password.
## Contributing
All are welcome to create pull requests for this project, against the `develop` branch. Official releases are created from the `master` branch.
CI is used in this project. All PR's must pass before being considered. After passing,
docker builds for PR's are available on dockerhub for manual verifications.
Documentation within the `develop` branch is available for preview at
[https://develop.nginxproxymanager.com](https://develop.nginxproxymanager.com)
### Contributors
Special thanks to [all of our contributors](https://github.com/NginxProxyManager/nginx-proxy-manager/graphs/contributors).
## Getting Support
1. [Found a bug?](https://github.com/NginxProxyManager/nginx-proxy-manager/issues)
2. [Discussions](https://github.com/NginxProxyManager/nginx-proxy-manager/discussions)
3. [Reddit](https://reddit.com/r/nginxproxymanager)
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center">
<a href="https://github.com/Subv">
<img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80px;" alt=""/>
<br /><sub><b>Sebastian Valle</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Indemnity83">
<img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80px;" alt=""/>
<br /><sub><b>Kyle Klaus</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/theraw">
<img src="https://avatars1.githubusercontent.com/u/32969774?s=460&u=6b359971e15685fb0359e6a8c065a399b40dc228&v=4" width="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" 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="80px;" alt=""/>
<br /><sub><b>Loris Bergeron</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";
import compression from "compression";
import express from "express";
import fileUpload from "express-fileupload";
import { isDebugMode } from "./lib/config.js";
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";
const express = require('express');
const bodyParser = require('body-parser');
const fileUpload = require('express-fileupload');
const compression = require('compression');
const log = require('./logger').express;
/**
* App
@@ -14,7 +10,7 @@ import mainRoutes from "./routes/main.js";
const app = express();
app.use(fileUpload());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.urlencoded({extended: true}));
// Gzip
app.use(compression());
@@ -23,70 +19,72 @@ app.use(compression());
* General Logging, BEFORE routes
*/
app.disable("x-powered-by");
app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]);
app.enable("strict routing");
app.disable('x-powered-by');
app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable('strict routing');
// pretty print JSON when not live
if (isDebugMode()) {
app.set("json spaces", 2);
if (process.env.NODE_ENV !== 'production') {
app.set('json spaces', 2);
}
// CORS for everything
app.use(cors);
app.use(require('./lib/express/cors'));
// General security/cache related headers + server header
app.use((_, res, next) => {
let x_frame_options = "DENY";
app.use(function (req, res, next) {
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;
}
res.set({
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": x_frame_options,
"Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
Pragma: "no-cache",
Expires: 0,
'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
'X-XSS-Protection': '1; mode=block',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': x_frame_options,
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Pragma: 'no-cache',
Expires: 0
});
next();
});
app.use(jwt());
app.use("/", mainRoutes);
app.use(require('./lib/express/jwt')());
app.use('/', require('./routes/api/main'));
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, _) => {
const payload = {
// eslint-disable-next-line
app.use(function (err, req, res, next) {
let payload = {
error: {
code: err.status,
message: err.public ? err.message : "Internal Error",
},
code: err.status,
message: err.public ? err.message : 'Internal Error'
}
};
if (typeof err.message_i18n !== "undefined") {
payload.error.message_i18n = err.message_i18n;
}
if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) {
if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
payload.debug = {
stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null,
previous: err.previous,
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous
};
}
// Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== "undefined" && err.stack) {
logger.debug(err.stack);
if (typeof err.public === "undefined" || !err.public) {
logger.warn(err.message);
if (typeof err.stack !== 'undefined' && err.stack) {
if (process.env.NODE_ENV === 'development') {
log.debug(err.stack);
} else if (typeof err.public == 'undefined' || !err.public) {
log.warn(err.message);
}
}
res.status(err.status || 500).send(payload);
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": {
"engine": "mysql2",
"engine": "mysql",
"host": "db",
"name": "npm",
"user": "npm",

View File

@@ -1,32 +1,33 @@
import knex from "knex";
import {configGet, configHas} from "./lib/config.js";
const config = require('config');
const generateDbConfig = () => {
if (!configHas("database")) {
throw new Error(
"Database config does not exist! Please read the instructions: https://nginxproxymanager.com/setup/",
);
}
if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md');
}
const 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 {
client: cfg.engine,
connection: {
host: cfg.host,
user: cfg.user,
password: cfg.password,
database: cfg.name,
port: cfg.port,
},
migrations: {
tableName: "migrations",
},
};
};
let data = generateDbConfig();
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,133 @@
#!/usr/bin/env node
import app from "./app.js";
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 logger = require('./logger').global;
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() {
return migrateUp()
const migrate = require('./migrate');
const setup = require('./setup');
const app = require('./app');
const apiValidator = require('./lib/validator/api');
const internalCertificate = require('./internal/certificate');
const internalIpRanges = require('./internal/ip_ranges');
return migrate.latest()
.then(setup)
.then(getCompiledSchema)
.then(() => {
if (!IP_RANGES_FETCH_ENABLED) {
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);
});
return apiValidator.loadSchemas;
})
.then(internalIpRanges.fetch)
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
const server = app.listen(3000, () => {
logger.info(`Backend PID ${process.pid} listening on port 3000 ...`);
logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
process.on("SIGTERM", () => {
logger.info(`PID ${process.pid} received SIGTERM`);
process.on('SIGTERM', () => {
logger.info('PID ' + process.pid + ' received SIGTERM');
server.close(() => {
logger.info("Stopping.");
logger.info('Stopping.');
process.exit(0);
});
});
});
})
.catch((err) => {
logger.error(`Startup Error: ${err.message}`, err);
logger.error(err.message);
setTimeout(appStart, 1000);
});
}
async function createDbConfigFromEnvironment() {
return new Promise((resolve, reject) => {
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlPort = process.env.DB_MYSQL_PORT || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envSqliteFile = process.env.DB_SQLITE_FILE || null;
if ((envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) || envSqliteFile) {
const fs = require('fs');
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
let configData = {};
try {
configData = require(filename);
} catch (err) {
// do nothing
}
if (configData.database && configData.database.engine && !configData.database.fromEnv) {
logger.info('Manual db configuration already exists, skipping config creation from environment variables');
resolve();
return;
}
if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) {
const newConfig = {
fromEnv: true,
engine: 'mysql',
host: envMysqlHost,
port: envMysqlPort,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating MySQL db configuration from environment variables');
configData.database = newConfig;
} else {
const newConfig = {
fromEnv: true,
engine: 'knex-native',
knex: {
client: 'sqlite3',
connection: {
filename: envSqliteFile
}
}
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating Sqlite db configuration from environment variables');
configData.database = newConfig;
}
// Write config
fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => {
if (err) {
logger.error('Could not write db config to config file: ' + filename);
reject(err);
} else {
logger.info('Wrote db configuration to config file: ' + filename);
resolve();
}
});
} else {
resolve();
}
});
}
try {
appStart();
} catch (err) {
logger.fatal(err);
logger.error(err.message, err);
process.exit(1);
}

View File

@@ -1,94 +1,103 @@
import fs from "node:fs";
import batchflow from "batchflow";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { access as logger } from "../logger.js";
import accessListModel from "../models/access_list.js";
import accessListAuthModel from "../models/access_list_auth.js";
import accessListClientModel from "../models/access_list_client.js";
import proxyHostModel from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalNginx from "./nginx.js";
const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error');
const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth');
const accessListClientModel = require('../models/access_list_client');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const utils = require('../lib/utils');
const omissions = () => {
return ["is_deleted"];
};
function omissions () {
return ['is_deleted'];
}
const internalAccessList = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
await access.can("access_lists:create", data);
const row = await accessListModel
.query()
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1),
create: (access, data) => {
return access.can('access_lists:create', data)
.then((/*access_data*/) => {
return accessListModel
.query()
.omit(omissions())
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1)
});
})
.then(utils.omitRow(omissions()));
.then((row) => {
data.id = row.id;
data.id = row.id;
let promises = [];
const promises = [];
// Items
data.items.map((item) => {
promises.push(
accessListAuthModel.query().insert({
access_list_id: row.id,
username: item.username,
password: item.password,
}),
);
return true;
});
// Now add the items
data.items.map((item) => {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: row.id,
username: item.username,
password: item.password
})
);
});
// Clients
data.clients?.map((client) => {
promises.push(
accessListClientModel.query().insert({
access_list_id: row.id,
address: client.address,
directive: client.directive,
}),
);
return true;
});
// Now add the clients
if (typeof data.clients !== 'undefined' && data.clients) {
data.clients.map((client) => {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: row.id,
address: client.address,
directive: client.directive
})
);
});
}
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
const freshRow = await internalAccessList.get(
access,
{
id: data.id,
expand: ["owner", "items", "clients", "proxy_hosts.access_list.[clients,items]"],
},
true // skip masking
);
// Audit log
data.meta = _.assign({}, data.meta || {}, freshRow.meta);
await internalAccessList.build(freshRow);
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
}
// Add to audit log
await internalAuditLog.add(access, {
action: "created",
object_type: "access-list",
object_id: freshRow.id,
meta: internalAccessList.maskItems(data),
});
return internalAccessList.maskItems(freshRow);
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
/**
@@ -99,107 +108,130 @@ const internalAccessList = {
* @param {String} [data.items]
* @return {Promise}
*/
update: async (access, data) => {
await access.can("access_lists:update", data.id);
const row = await internalAccessList.get(access, { id: data.id });
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.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);
update: (access, data) => {
return access.can('access_lists:update', data.id)
.then((/*access_data*/) => {
return internalAccessList.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
return true;
});
const query = accessListAuthModel.query().delete().where("access_list_id", data.id);
if (itemsToKeep.length) {
query.andWhere("username", "NOT IN", itemsToKeep);
}
await query;
// Add new items
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,
}),
);
})
.then(() => {
// patch name if specified
if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
.query()
.where({id: data.id})
.patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
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(() => {
// 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 {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skipMasking]
* @param {Boolean} [skip_masking]
* @return {Promise}
*/
get: async (access, data, skipMasking) => {
const thisData = data || {};
const accessData = await access.can("access_lists:get", thisData.id)
get: (access, data, skip_masking) => {
if (typeof data === 'undefined') {
data = {};
}
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
return access.can('access_lists:get', data.id)
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
.omit(['access_list.is_deleted'])
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.where("access_list.is_deleted", 0)
.andWhere("access_list.id", thisData.id)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]")
.first();
.then((row) => {
if (row) {
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
if (accessData.permission_visibility !== "all") {
query.andWhere("access_list.owner_user_id", access.token.getUserId(1));
}
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;
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
@@ -261,64 +295,73 @@ const internalAccessList = {
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("access_lists:delete", data.id);
const row = await internalAccessList.get(access, {
id: data.id,
expand: ["proxy_hosts", "items", "clients"],
});
delete: (access, data) => {
return access.can('access_lists:delete', data.id)
.then(() => {
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
return accessListModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where('access_list_id', '=', row.id)
.patch({access_list_id: 0})
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// 1. update row to be deleted
await accessListModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
});
// set the access_list_id to zero for these items
row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0;
});
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
await proxyHostModel
.query()
.where("access_list_id", "=", row.id)
.patch({ access_list_id: 0 });
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
})
.then(() => {
return internalNginx.reload();
});
}
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map((_val, idx) => {
row.proxy_hosts[idx].access_list_id = 0;
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
});
})
.then(() => {
return true;
});
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 {Array} [expand]
* @param {String} [searchQuery]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("access_lists:list");
getAll: (access, expand, search_query) => {
return access.can('access_lists:list')
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.omit(['access_list.is_deleted'])
.allowEager('[owner,items,clients]')
.orderBy('access_list.name', 'ASC');
const query = accessListModel
.query()
.select("access_list.*", accessListModel.raw("COUNT(proxy_host.id) as proxy_host_count"))
.leftJoin("proxy_host", function () {
this.on("proxy_host.access_list_id", "=", "access_list.id").andOn(
"proxy_host.is_deleted",
"=",
0,
);
})
.where("access_list.is_deleted", 0)
.groupBy("access_list.id")
.allowGraph("[owner,items,clients]")
.orderBy("access_list.name", "ASC");
if (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);
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
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
* @returns {Promise}
*/
getCount: async (userId, visibility) => {
const query = accessListModel
getCount: (user_id, visibility) => {
let query = accessListModel
.query()
.count("id as count")
.where("is_deleted", 0);
.count('id as count')
.where('is_deleted', 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", userId);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
const row = await query.first();
return Number.parseInt(row.count, 10);
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
},
/**
@@ -400,21 +443,21 @@ const internalAccessList = {
* @returns {Object}
*/
maskItems: (list) => {
if (list && typeof list.items !== "undefined") {
list.items.map((val, idx) => {
let repeatFor = 8;
let firstChar = "*";
if (list && typeof list.items !== 'undefined') {
list.items.map(function (val, idx) {
let repeat_for = 8;
let first_char = '*';
if (typeof val.password !== "undefined" && val.password) {
repeatFor = val.password.length - 1;
firstChar = val.password.charAt(0);
if (typeof val.password !== 'undefined' && val.password) {
repeat_for = val.password.length - 1;
first_char = val.password.charAt(0);
}
list.items[idx].hint = firstChar + "*".repeat(repeatFor);
list.items[idx].password = "";
return true;
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
list.items[idx].password = '';
});
}
return list;
},
@@ -424,7 +467,7 @@ const internalAccessList = {
* @returns {String}
*/
getFilename: (list) => {
return `/data/access/${list.id}`;
return '/data/access/' + list.id;
},
/**
@@ -434,55 +477,58 @@ const internalAccessList = {
* @param {Array} list.items
* @returns {Promise}
*/
build: async (list) => {
logger.info(`Building Access file #${list.id} for: ${list.name}`);
build: (list) => {
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
try {
fs.unlinkSync(htpasswdFile);
} catch (_err) {
// do nothing
}
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
// 2. create empty access file
fs.writeFileSync(htpasswdFile, '', {encoding: 'utf8'});
// 2. create empty access file
try {
fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
})
.then((htpasswd_file) => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
// 3. generate password for each user
if (list.items.length) {
await new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((_i, item, next) => {
if (item.password?.length) {
logger.info(`Adding: ${item.username}`);
utils.execFile('openssl', ['passwd', '-apr1', item.password])
.then((res) => {
try {
fs.appendFileSync(htpasswdFile, `${item.username}:${res}\n`, {encoding: 'utf8'});
} catch (err) {
reject(err);
}
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success(`Built Access file #${list.id} for: ${list.name}`);
resolve(results);
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
.then((/*result*/) => {
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results);
});
});
}
});
}
}
}
};
export default internalAccessList;
module.exports = internalAccessList;

View File

@@ -1,6 +1,5 @@
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import auditLogModel from "../models/audit-log.js";
const error = require('../lib/error');
const auditLogModel = require('../models/audit-log');
const internalAuditLog = {
@@ -9,31 +8,32 @@ const internalAuditLog = {
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [searchQuery]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
await access.can("auditlog:list");
getAll: (access, expand, search_query) => {
return access.can('auditlog:list')
.then(() => {
let query = auditLogModel
.query()
.orderBy('created_on', 'DESC')
.orderBy('id', 'DESC')
.limit(100)
.allowEager('[user]');
const query = auditLogModel
.query()
.orderBy("created_on", "DESC")
.orderBy("id", "DESC")
.limit(100)
.allowGraph("[user]");
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('meta', 'like', '%' + search_query + '%');
});
}
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("meta"), "like", `%${searchQuery}`);
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
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]
* @returns {Promise}
*/
add: async (access, data) => {
if (typeof data.user_id === "undefined" || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
add: (access, data) => {
return new Promise((resolve, reject) => {
// Default the user id
if (typeof data.user_id === 'undefined' || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
if (typeof data.action === "undefined" || !data.action) {
throw new errs.InternalValidationError("Audit log entry must contain an Action");
}
// Make sure at least 1 of the IDs are set and action
return await auditLogModel.query().insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || "",
object_id: data.object_id || 0,
meta: data.meta || {},
if (typeof data.action === 'undefined' || !data.action) {
reject(new error.InternalValidationError('Audit log entry must contain an Action'));
} else {
// Make sure at least 1 of the IDs are set and action
resolve(auditLogModel
.query()
.insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || '',
object_id: data.object_id || 0,
meta: data.meta || {}
}));
}
});
},
}
};
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";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import deadHostModel from "../models/dead_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const _ = require('lodash');
const error = require('../lib/error');
const deadHostModel = require('../models/dead_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const omissions = () => {
return ["is_deleted"];
};
function omissions () {
return ['is_deleted'];
}
const internalDeadHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
const createCertificate = data.certificate_id === "new";
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (createCertificate) {
if (create_certificate) {
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
const domainNameCheckPromises = [];
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
});
data.domain_names.map((domain_name) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domain_name));
return true;
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
await Promise.all(domainNameCheckPromises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
return deadHostModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
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
* @return {Promise}
*/
update: async (access, data) => {
const createCertificate = data.certificate_id === "new";
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (createCertificate) {
if (create_certificate) {
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
const domainNameCheckPromises = [];
if (typeof data.domain_names !== "undefined") {
data.domain_names.map((domainName) => {
domainNameCheckPromises.push(internalHost.isHostnameTaken(domainName, "dead", data.id));
return true;
});
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id));
});
const checkResults = await Promise.all(domainNameCheckPromises);
checkResults.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
}
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]
* @return {Promise}
*/
get: async (access, data) => {
const accessData = await access.can("dead_hosts:get", data.id);
const query = deadHostModel
.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));
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
}
if (typeof data.expand !== "undefined" && data.expand !== null) {
query.withGraphFetched(`[${data.expand.join(", ")}]`);
}
return access.can('dead_hosts:get', data.id)
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[owner,certificate]')
.first();
const row = await query.then(utils.omitRow(omissions()));
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
// Custom omissions
if (typeof data.omit !== "undefined" && data.omit !== null) {
return _.omit(row, data.omit);
}
return row;
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
@@ -210,30 +253,42 @@ const internalDeadHost = {
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: async (access, data) => {
await access.can("dead_hosts:delete", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
delete: (access, data) => {
return access.can('dead_hosts:delete', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
await deadHostModel
.query()
.where("id", row.id)
.patch({
is_deleted: 1,
return deadHostModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
// 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]
* @returns {Promise}
*/
enable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
});
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
}
enable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
row.enabled = 1;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 1,
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
// 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]
* @returns {Promise}
*/
disable: async (access, data) => {
await access.can("dead_hosts:update", data.id)
const row = await internalDeadHost.get(access, { id: data.id });
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
}
disable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
row.enabled = 0;
await deadHostModel
.query()
.where("id", row.id)
.patch({
enabled: 0,
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
// 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 {Array} [expand]
* @param {String} [searchQuery]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("dead_hosts:list")
const query = deadHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
getAll: (access, expand, search_query) => {
return access.can('dead_hosts:list')
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[owner,certificate]')
.orderBy('domain_names', 'ASC');
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
}
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
* @returns {Promise}
*/
getCount: async (user_id, visibility) => {
const query = deadHostModel.query().count("id as count").where("is_deleted", 0);
getCount: (user_id, visibility) => {
let query = deadHostModel
.query()
.count('id as count')
.where('is_deleted', 0);
if (visibility !== "all") {
query.andWhere("owner_user_id", user_id);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
const row = await query.first();
return Number.parseInt(row.count, 10);
},
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
};
export default internalDeadHost;
module.exports = internalDeadHost;

View File

@@ -1,10 +1,10 @@
import _ from "lodash";
import { castJsonIfNeed } from "../lib/helpers.js";
import deadHostModel from "../models/dead_host.js";
import proxyHostModel from "../models/proxy_host.js";
import redirectionHostModel from "../models/redirection_host.js";
const _ = require('lodash');
const proxyHostModel = require('../models/proxy_host');
const redirectionHostModel = require('../models/redirection_host');
const deadHostModel = require('../models/dead_host');
const internalHost = {
/**
* Makes sure that the ssl_* and hsts_* fields play nicely together.
* ie: if there is no cert, then force_ssl is off.
@@ -14,23 +14,25 @@ const internalHost = {
* @param {object} [existing_data]
* @returns {object}
*/
cleanSslHstsData: (data, existingData) => {
const combinedData = _.assign({}, existingData || {}, data);
cleanSslHstsData: function (data, existing_data) {
existing_data = existing_data === undefined ? {} : existing_data;
if (!combinedData.certificate_id) {
combinedData.ssl_forced = false;
combinedData.http2_support = false;
let combined_data = _.assign({}, existing_data, data);
if (!combined_data.certificate_id) {
combined_data.ssl_forced = false;
combined_data.http2_support = false;
}
if (!combinedData.ssl_forced) {
combinedData.hsts_enabled = false;
if (!combined_data.ssl_forced) {
combined_data.hsts_enabled = false;
}
if (!combinedData.hsts_enabled) {
combinedData.hsts_subdomains = false;
if (!combined_data.hsts_enabled) {
combined_data.hsts_subdomains = false;
}
return combinedData;
return combined_data;
},
/**
@@ -39,12 +41,11 @@ const internalHost = {
* @param {Array} rows
* @returns {Array}
*/
cleanAllRowsCertificateMeta: (rows) => {
rows.map((_, idx) => {
if (typeof rows[idx].certificate !== "undefined" && rows[idx].certificate) {
cleanAllRowsCertificateMeta: function (rows) {
rows.map(function (row, idx) {
if (typeof rows[idx].certificate !== 'undefined' && rows[idx].certificate) {
rows[idx].certificate.meta = {};
}
return true;
});
return rows;
@@ -56,8 +57,8 @@ const internalHost = {
* @param {Object} row
* @returns {Object}
*/
cleanRowCertificateMeta: (row) => {
if (typeof row.certificate !== "undefined" && row.certificate) {
cleanRowCertificateMeta: function (row) {
if (typeof row.certificate !== 'undefined' && row.certificate) {
row.certificate.meta = {};
}
@@ -65,33 +66,54 @@ 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
*
* @param {Array} domainNames
* @param {Array} domain_names
* @returns {Promise}
*/
getHostsWithDomains: async (domainNames) => {
const responseObject = {
total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: [],
};
getHostsWithDomains: function (domain_names) {
let promises = [
proxyHostModel
.query()
.where('is_deleted', 0),
redirectionHostModel
.query()
.where('is_deleted', 0),
deadHostModel
.query()
.where('is_deleted', 0)
];
const proxyRes = await proxyHostModel.query().where("is_deleted", 0);
responseObject.proxy_hosts = internalHost._getHostsWithDomains(proxyRes, domainNames);
responseObject.total_count += responseObject.proxy_hosts.length;
return Promise.all(promises)
.then((promises_results) => {
let response_object = {
total_count: 0,
dead_hosts: [],
proxy_hosts: [],
redirection_hosts: []
};
const redirRes = await redirectionHostModel.query().where("is_deleted", 0);
responseObject.redirection_hosts = internalHost._getHostsWithDomains(redirRes, domainNames);
responseObject.total_count += responseObject.redirection_hosts.length;
if (promises_results[0]) {
// Proxy Hosts
response_object.proxy_hosts = internalHost._getHostsWithDomains(promises_results[0], domain_names);
response_object.total_count += response_object.proxy_hosts.length;
}
const deadRes = await deadHostModel.query().where("is_deleted", 0);
responseObject.dead_hosts = internalHost._getHostsWithDomains(deadRes, domainNames);
responseObject.total_count += responseObject.dead_hosts.length;
if (promises_results[1]) {
// Redirection Hosts
response_object.redirection_hosts = internalHost._getHostsWithDomains(promises_results[1], domain_names);
response_object.total_count += response_object.redirection_hosts.length;
}
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;
}
return response_object;
});
},
/**
@@ -102,133 +124,112 @@ const internalHost = {
* @param {Integer} [ignore_id] Must be supplied if type was also supplied
* @returns {Promise}
*/
isHostnameTaken: (hostname, ignore_type, ignore_id) => {
const promises = [
isHostnameTaken: function (hostname, ignore_type, ignore_id) {
let promises = [
proxyHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
.where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%'),
redirectionHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
.where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%'),
deadHostModel
.query()
.where("is_deleted", 0)
.andWhere(castJsonIfNeed("domain_names"), "like", `%${hostname}%`),
.where('is_deleted', 0)
.andWhere('domain_names', 'like', '%' + hostname + '%')
];
return Promise.all(promises).then((promises_results) => {
let is_taken = false;
return Promise.all(promises)
.then((promises_results) => {
let is_taken = false;
if (promises_results[0]) {
// Proxy Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[0],
ignore_type === "proxy" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
if (promises_results[0]) {
// Proxy Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[0], ignore_type === 'proxy' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
}
}
if (promises_results[1]) {
// Redirection Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[1],
ignore_type === "redirection" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
if (promises_results[1]) {
// Redirection Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[1], ignore_type === 'redirection' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
}
}
if (promises_results[2]) {
// Dead Hosts
if (
internalHost._checkHostnameRecordsTaken(
hostname,
promises_results[2],
ignore_type === "dead" && ignore_id ? ignore_id : 0,
)
) {
is_taken = true;
if (promises_results[2]) {
// Dead Hosts
if (internalHost._checkHostnameRecordsTaken(hostname, promises_results[2], ignore_type === 'dead' && ignore_id ? ignore_id : 0)) {
is_taken = true;
}
}
}
return {
hostname: hostname,
is_taken: is_taken,
};
});
return {
hostname: hostname,
is_taken: is_taken
};
});
},
/**
* Private call only
*
* @param {String} hostname
* @param {Array} existingRows
* @param {Integer} [ignoreId]
* @param {Array} existing_rows
* @param {Integer} [ignore_id]
* @returns {Boolean}
*/
_checkHostnameRecordsTaken: (hostname, existingRows, ignoreId) => {
let isTaken = false;
_checkHostnameRecordsTaken: function (hostname, existing_rows, ignore_id) {
let is_taken = false;
if (existingRows?.length) {
existingRows.map((existingRow) => {
existingRow.domain_names.map((existingHostname) => {
if (existing_rows && existing_rows.length) {
existing_rows.map(function (existing_row) {
existing_row.domain_names.map(function (existing_hostname) {
// Does this domain match?
if (existingHostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignoreId || ignoreId !== existingRow.id) {
isTaken = true;
if (existing_hostname.toLowerCase() === hostname.toLowerCase()) {
if (!ignore_id || ignore_id !== existing_row.id) {
is_taken = true;
}
}
return true;
});
return true;
});
}
return isTaken;
return is_taken;
},
/**
* Private call only
*
* @param {Array} hosts
* @param {Array} domainNames
* @param {Array} domain_names
* @returns {Array}
*/
_getHostsWithDomains: (hosts, domainNames) => {
const response = [];
_getHostsWithDomains: function (hosts, domain_names) {
let response = [];
if (hosts?.length) {
hosts.map((host) => {
let hostMatches = false;
if (hosts && hosts.length) {
hosts.map(function (host) {
let host_matches = false;
domainNames.map((domainName) => {
host.domain_names.map((hostDomainName) => {
if (domainName.toLowerCase() === hostDomainName.toLowerCase()) {
hostMatches = true;
domain_names.map(function (domain_name) {
host.domain_names.map(function (host_domain_name) {
if (domain_name.toLowerCase() === host_domain_name.toLowerCase()) {
host_matches = true;
}
return true;
});
return true;
});
if (hostMatches) {
if (host_matches) {
response.push(host);
}
return true;
});
}
return response;
},
}
};
export default internalHost;
module.exports = internalHost;

View File

@@ -1,51 +1,42 @@
import fs from "node:fs";
import https from "node:https";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { ipRanges as logger } from "../logger.js";
import internalNginx from "./nginx.js";
const https = require('https');
const fs = require('fs');
const logger = require('../logger').ip_ranges;
const error = require('../lib/error');
const internalNginx = require('./nginx');
const { Liquid } = require('liquidjs');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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 CLOUDFRONT_URL = 'https://ip-ranges.amazonaws.com/ip-ranges.json';
const CLOUDFARE_V4_URL = 'https://www.cloudflare.com/ips-v4';
const CLOUDFARE_V6_URL = 'https://www.cloudflare.com/ips-v6';
const internalIpRanges = {
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_timeout: 1000 * 60 * 60 * 6, // 6 hours
interval: null,
interval_processing: false,
iteration_count: 0,
iteration_count: 0,
initTimer: () => {
logger.info("IP Ranges Renewal Timer initialized");
logger.info('IP Ranges Renewal Timer initialized');
internalIpRanges.interval = setInterval(internalIpRanges.fetch, internalIpRanges.interval_timeout);
},
fetchUrl: (url) => {
return new Promise((resolve, reject) => {
logger.info(`Fetching ${url}`);
return https
.get(url, (res) => {
res.setEncoding("utf8");
let raw_data = "";
res.on("data", (chunk) => {
raw_data += chunk;
});
res.on("end", () => {
resolve(raw_data);
});
})
.on("error", (err) => {
reject(err);
logger.info('Fetching ' + url);
return https.get(url, (res) => {
res.setEncoding('utf8');
let raw_data = '';
res.on('data', (chunk) => {
raw_data += chunk;
});
res.on('end', () => {
resolve(raw_data);
});
}).on('error', (err) => {
reject(err);
});
});
},
@@ -55,30 +46,27 @@ const internalIpRanges = {
fetch: () => {
if (!internalIpRanges.interval_processing) {
internalIpRanges.interval_processing = true;
logger.info("Fetching IP Ranges from online services...");
logger.info('Fetching IP Ranges from online services...');
let ip_ranges = [];
return internalIpRanges
.fetchUrl(CLOUDFRONT_URL)
return internalIpRanges.fetchUrl(CLOUDFRONT_URL)
.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) => {
if (item.service === "CLOUDFRONT") {
if (item.service === 'CLOUDFRONT') {
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) => {
if (item.service === "CLOUDFRONT") {
if (item.service === 'CLOUDFRONT') {
ip_ranges.push(item.ipv6_prefix);
}
return true;
});
}
})
@@ -86,38 +74,38 @@ const internalIpRanges = {
return internalIpRanges.fetchUrl(CLOUDFARE_V4_URL);
})
.then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV4.test(line));
ip_ranges = [...ip_ranges, ...items];
let items = cloudfare_data.split('\n');
ip_ranges = [... ip_ranges, ... items];
})
.then(() => {
return internalIpRanges.fetchUrl(CLOUDFARE_V6_URL);
})
.then((cloudfare_data) => {
const items = cloudfare_data.split("\n").filter((line) => regIpV6.test(line));
ip_ranges = [...ip_ranges, ...items];
let items = cloudfare_data.split('\n');
ip_ranges = [... ip_ranges, ... items];
})
.then(() => {
const clean_ip_ranges = [];
let clean_ip_ranges = [];
ip_ranges.map((range) => {
if (range) {
clean_ip_ranges.push(range);
}
return true;
});
return internalIpRanges.generateConfig(clean_ip_ranges).then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
});
return internalIpRanges.generateConfig(clean_ip_ranges)
.then(() => {
if (internalIpRanges.iteration_count) {
// Reload nginx
return internalNginx.reload();
}
});
})
.then(() => {
internalIpRanges.interval_processing = false;
internalIpRanges.iteration_count++;
})
.catch((err) => {
logger.fatal(err.message);
logger.error(err.message);
internalIpRanges.interval_processing = false;
});
}
@@ -128,29 +116,32 @@ const internalIpRanges = {
* @returns {Promise}
*/
generateConfig: (ip_ranges) => {
const renderEngine = utils.getRenderEngine();
let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => {
let template = null;
const filename = "/etc/nginx/conf.d/include/ip_ranges.conf";
let filename = '/etc/nginx/conf.d/include/ip_ranges.conf';
try {
template = fs.readFileSync(`${__dirname}/../templates/ip_ranges.conf`, { encoding: "utf8" });
template = fs.readFileSync(__dirname + '/../templates/ip_ranges.conf', {encoding: 'utf8'});
} catch (err) {
reject(new errs.ConfigurationError(err.message));
reject(new error.ConfigurationError(err.message));
return;
}
renderEngine
.parseAndRender(template, { ip_ranges: ip_ranges })
.parseAndRender(template, {ip_ranges: ip_ranges})
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
resolve(true);
})
.catch((err) => {
logger.warn(`Could not write ${filename}: ${err.message}`);
reject(new errs.ConfigurationError(err.message));
logger.warn('Could not write ' + filename + ':', err.message);
reject(new error.ConfigurationError(err.message));
});
});
},
}
};
export default internalIpRanges;
module.exports = internalIpRanges;

View File

@@ -1,15 +1,13 @@
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import { nginx as logger } from "../logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const _ = require('lodash');
const fs = require('fs');
const logger = require('../logger').nginx;
const utils = require('../lib/utils');
const error = require('../lib/error');
const { Liquid } = require('liquidjs');
const debug_mode = process.env.NODE_ENV !== 'production' || !!process.env.DEBUG;
const internalNginx = {
/**
* This will:
* - test the nginx config first to make sure it's OK
@@ -27,63 +25,60 @@ const internalNginx = {
configure: (model, host_type, host) => {
let combined_meta = {};
return internalNginx
.test()
return internalNginx.test()
.then(() => {
// Nginx is OK
// We're deleting this config regardless.
// Don't throw errors, as the file may not exist at all
// Delete the .err file too
return internalNginx.deleteConfig(host_type, host, false, true);
return internalNginx.deleteConfig(host_type, host); // Don't throw errors, as the file may not exist at all
})
.then(() => {
return internalNginx.generateConfig(host_type, host);
})
.then(() => {
// Test nginx again and update meta with result
return internalNginx
.test()
return internalNginx.test()
.then(() => {
// nginx is ok
combined_meta = _.assign({}, host.meta, {
nginx_online: true,
nginx_err: null,
nginx_err: null
});
return model.query().where("id", host.id).patch({
meta: combined_meta,
});
return model
.query()
.where('id', host.id)
.patch({
meta: combined_meta
});
})
.catch((err) => {
// Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported.
// It will always look like this:
// nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address)
const valid_lines = [];
const err_lines = err.message.split("\n");
err_lines.map((line) => {
if (line.indexOf("/var/log/nginx/error.log") === -1) {
let valid_lines = [];
let err_lines = err.message.split('\n');
err_lines.map(function (line) {
if (line.indexOf('/var/log/nginx/error.log') === -1) {
valid_lines.push(line);
}
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
combined_meta = _.assign({}, host.meta, {
nginx_online: false,
nginx_err: valid_lines.join("\n"),
nginx_err: valid_lines.join('\n')
});
return model
.query()
.where("id", host.id)
.where('id', host.id)
.patch({
meta: combined_meta,
})
.then(() => {
internalNginx.renameConfigAsError(host_type, host);
meta: combined_meta
})
.then(() => {
return internalNginx.deleteConfig(host_type, host, true);
@@ -102,18 +97,22 @@ const internalNginx = {
* @returns {Promise}
*/
test: () => {
logger.debug("Testing Nginx configuration");
return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]);
if (debug_mode) {
logger.info('Testing Nginx configuration');
}
return utils.exec('/usr/sbin/nginx -t -g "error_log off;"');
},
/**
* @returns {Promise}
*/
reload: () => {
return internalNginx.test().then(() => {
logger.info("Reloading Nginx");
return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]);
});
return internalNginx.test()
.then(() => {
logger.info('Reloading Nginx');
return utils.exec('/usr/sbin/nginx -s reload');
});
},
/**
@@ -122,10 +121,13 @@ const internalNginx = {
* @returns {String}
*/
getConfigName: (host_type, host_id) => {
if (host_type === "default") {
return "/data/nginx/default_host/site.conf";
host_type = host_type.replace(new RegExp('-', 'g'), '_');
if (host_type === 'default') {
return '/data/nginx/default_host/site.conf';
}
return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`;
return '/data/nginx/' + host_type + '/' + host_id + '.conf';
},
/**
@@ -138,41 +140,28 @@ const internalNginx = {
let template;
try {
template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" });
template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
} catch (err) {
reject(new errs.ConfigurationError(err.message));
reject(new error.ConfigurationError(err.message));
return;
}
const renderEngine = utils.getRenderEngine();
let renderedLocations = "";
let renderer = new Liquid();
let renderedLocations = '';
const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) {
const locationCopy = Object.assign(
{},
{ access_list_id: host.access_list_id },
{ certificate_id: host.certificate_id },
{ ssl_forced: host.ssl_forced },
{ caching_enabled: host.caching_enabled },
{ block_exploits: host.block_exploits },
{ allow_websocket_upgrade: host.allow_websocket_upgrade },
{ http2_support: host.http2_support },
{ hsts_enabled: host.hsts_enabled },
{ hsts_subdomains: host.hsts_subdomains },
{ access_list: host.access_list },
{ certificate: host.certificate },
host.locations[i],
);
let locationCopy = Object.assign({}, host.locations[i]);
if (locationCopy.forward_host.indexOf("/") > -1) {
const splitted = locationCopy.forward_host.split("/");
if (locationCopy.forward_host.indexOf('/') > -1) {
const splitted = locationCopy.forward_host.split('/');
locationCopy.forward_host = splitted.shift();
locationCopy.forward_path = `/${splitted.join("/")}`;
locationCopy.forward_path = `/${splitted.join('/')}`;
}
renderedLocations += await renderEngine.parseAndRender(template, locationCopy);
// eslint-disable-next-line
renderedLocations += await renderer.parseAndRender(template, locationCopy);
}
};
@@ -185,23 +174,25 @@ const internalNginx = {
* @param {Object} host
* @returns {Promise}
*/
generateConfig: (host_type, host_row) => {
// Prevent modifying the original object:
const host = JSON.parse(JSON.stringify(host_row));
const nice_host_type = internalNginx.getFileFriendlyHostType(host_type);
generateConfig: (host_type, host) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
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();
let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => {
let template = null;
const filename = internalNginx.getConfigName(nice_host_type, host.id);
let filename = internalNginx.getConfigName(host_type, host.id);
try {
template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" });
template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
} catch (err) {
reject(new errs.ConfigurationError(err.message));
reject(new error.ConfigurationError(err.message));
return;
}
@@ -209,26 +200,26 @@ const internalNginx = {
let origLocations;
// Manipulate the data a bit before sending it to the template
if (nice_host_type !== "default") {
if (host_type !== 'default') {
host.use_default_location = true;
if (typeof host.advanced_config !== "undefined" && host.advanced_config) {
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
if (host.locations) {
//logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2));
origLocations = [].concat(host.locations);
origLocations = [].concat(host.locations);
locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
host.locations = renderedLocations;
});
// Allow someone who is using / custom location path to use it, and skip the default / location
_.map(host.locations, (location) => {
if (location.path === "/") {
if (location.path === '/') {
host.use_default_location = false;
}
});
} else {
locationsPromise = Promise.resolve();
}
@@ -240,8 +231,11 @@ const internalNginx = {
renderEngine
.parseAndRender(template, host)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
logger.debug("Wrote config:", filename, 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;
@@ -249,8 +243,11 @@ const internalNginx = {
resolve(true);
})
.catch((err) => {
logger.debug(`Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
if (debug_mode) {
logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
});
});
});
@@ -265,17 +262,22 @@ const internalNginx = {
* @returns {Promise}
*/
generateLetsEncryptRequestConfig: (certificate) => {
logger.debug("Generating LetsEncrypt Request Config:", certificate);
const renderEngine = utils.getRenderEngine();
if (debug_mode) {
logger.info('Generating LetsEncrypt Request Config:', certificate);
}
let renderEngine = new Liquid({
root: __dirname + '/../templates/'
});
return new Promise((resolve, reject) => {
let template = null;
const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
let filename = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
try {
template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" });
template = fs.readFileSync(__dirname + '/../templates/letsencrypt-request.conf', {encoding: 'utf8'});
} catch (err) {
reject(new errs.ConfigurationError(err.message));
reject(new error.ConfigurationError(err.message));
return;
}
@@ -284,72 +286,51 @@ const internalNginx = {
renderEngine
.parseAndRender(template, certificate)
.then((config_text) => {
fs.writeFileSync(filename, config_text, { encoding: "utf8" });
logger.debug("Wrote config:", filename, config_text);
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (debug_mode) {
logger.success('Wrote config:', filename, config_text);
}
resolve(true);
})
.catch((err) => {
logger.debug(`Could not write ${filename}:`, err.message);
reject(new errs.ConfigurationError(err.message));
if (debug_mode) {
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`
*
* @param {Object} certificate
* @param {Boolean} [throw_errors]
* @returns {Promise}
*/
deleteLetsEncryptRequestConfig: (certificate) => {
const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`;
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
resolve();
});
},
deleteLetsEncryptRequestConfig: (certificate, throw_errors) => {
return new Promise((resolve, reject) => {
try {
let config_file = '/data/nginx/temp/letsencrypt_' + certificate.id + '.conf';
/**
* @param {String} host_type
* @param {Object} [host]
* @param {Boolean} [delete_err_file]
* @returns {Promise}
*/
deleteConfig: (host_type, host, delete_err_file) => {
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
if (debug_mode) {
logger.warn('Deleting nginx config: ' + config_file);
}
return new Promise((resolve /*, reject*/) => {
internalNginx.deleteFile(config_file);
if (delete_err_file) {
internalNginx.deleteFile(config_file_err);
fs.unlinkSync(config_file);
} catch (err) {
if (debug_mode) {
logger.warn('Could not delete config:', err.message);
}
if (throw_errors) {
reject(err);
}
}
resolve();
});
},
@@ -357,23 +338,32 @@ const internalNginx = {
/**
* @param {String} host_type
* @param {Object} [host]
* @param {Boolean} [throw_errors]
* @returns {Promise}
*/
renameConfigAsError: (host_type, host) => {
const config_file = internalNginx.getConfigName(
internalNginx.getFileFriendlyHostType(host_type),
typeof host === "undefined" ? 0 : host.id,
);
const config_file_err = `${config_file}.err`;
deleteConfig: (host_type, host, throw_errors) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
return new Promise((resolve /*, reject*/) => {
fs.unlink(config_file, () => {
// ignore result, continue
fs.rename(config_file, config_file_err, () => {
// also ignore result, as this is a debugging informative file anyway
resolve();
});
});
return new Promise((resolve, reject) => {
try {
let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
if (debug_mode) {
logger.warn('Deleting nginx config: ' + config_file);
}
fs.unlinkSync(config_file);
} catch (err) {
if (debug_mode) {
logger.warn('Could not delete config:', err.message);
}
if (throw_errors) {
reject(err);
}
}
resolve();
});
},
@@ -383,10 +373,9 @@ const internalNginx = {
* @returns {Promise}
*/
bulkGenerateConfigs: (host_type, hosts) => {
const promises = [];
hosts.map((host) => {
let promises = [];
hosts.map(function (host) {
promises.push(internalNginx.generateConfig(host_type, host));
return true;
});
return Promise.all(promises);
@@ -395,13 +384,13 @@ const internalNginx = {
/**
* @param {String} host_type
* @param {Array} hosts
* @param {Boolean} [throw_errors]
* @returns {Promise}
*/
bulkDeleteConfigs: (host_type, hosts) => {
const promises = [];
hosts.map((host) => {
promises.push(internalNginx.deleteConfig(host_type, host, true));
return true;
bulkDeleteConfigs: (host_type, hosts, throw_errors) => {
let promises = [];
hosts.map(function (host) {
promises.push(internalNginx.deleteConfig(host_type, host, throw_errors));
});
return Promise.all(promises);
@@ -411,19 +400,21 @@ const internalNginx = {
* @param {string} config
* @returns {boolean}
*/
advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im),
advancedConfigHasDefaultLocation: function (config) {
return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
},
/**
* @returns {boolean}
*/
ipv6Enabled: () => {
if (typeof process.env.DISABLE_IPV6 !== "undefined") {
ipv6Enabled: function () {
if (typeof process.env.DISABLE_IPV6 !== 'undefined') {
const disabled = process.env.DISABLE_IPV6.toLowerCase();
return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes");
return !(disabled === 'on' || disabled === 'true' || disabled === '1' || disabled === 'yes');
}
return true;
},
}
};
export default internalNginx;
module.exports = internalNginx;

View File

@@ -1,106 +1,99 @@
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import proxyHostModel from "../models/proxy_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const _ = require('lodash');
const error = require('../lib/error');
const proxyHostModel = require('../models/proxy_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const omissions = () => {
return ["is_deleted", "owner.is_deleted"];
};
function omissions () {
return ['is_deleted'];
}
const internalProxyHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let thisData = data;
const createCertificate = thisData.certificate_id === "new";
let create_certificate = data.certificate_id === 'new';
if (createCertificate) {
delete thisData.certificate_id;
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("proxy_hosts:create", thisData)
return access.can('proxy_hosts:create', data)
.then(() => {
// 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));
return true;
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
});
})
.then(() => {
// At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value
// for this optional field.
if (typeof thisData.advanced_config === "undefined") {
thisData.advanced_config = "";
}
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
return proxyHostModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((row) => {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalProxyHost.update(access, {
id: row.id,
certificate_id: cert.id,
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalProxyHost.get(access, {
id: row.id,
expand: ["certificate", "owner", "access_list.[clients,items]"],
id: row.id,
expand: ['certificate', 'owner', 'access_list.[clients,items]']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
return row;
});
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
.then(() => {
return row;
});
})
.then((row) => {
// Audit log
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
return internalAuditLog.add(access, {
action: 'created',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@@ -114,110 +107,99 @@ const internalProxyHost = {
* @return {Promise}
*/
update: (access, data) => {
let thisData = data;
const create_certificate = thisData.certificate_id === "new";
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete thisData.certificate_id;
delete data.certificate_id;
}
return access
.can("proxy_hosts:update", thisData.id)
return access.can('proxy_hosts:update', data.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
let domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
return domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "proxy", thisData.id),
);
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'proxy', data.id));
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
});
}
})
.then(() => {
return internalProxyHost.get(access, { id: thisData.id });
return internalProxyHost.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== thisData.id) {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Proxy Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
throw new error.InternalValidationError('Proxy Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
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
thisData.certificate_id = cert.id;
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
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,
},
data,
);
data = _.assign({}, {
domain_names: row.domain_names
}, data);
thisData = internalHost.cleanSslHstsData(thisData, row);
data = internalHost.cleanSslHstsData(data, row);
return proxyHostModel
.query()
.where({ id: thisData.id })
.patch(thisData)
.then(utils.omitRow(omissions()))
.where({id: data.id})
.patch(data)
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "proxy-host",
object_id: row.id,
meta: thisData,
})
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return saved_row;
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalProxyHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate", "access_list.[clients,items]"],
})
return internalProxyHost.get(access, {
id: data.id,
expand: ['owner', 'certificate', 'access_list.[clients,items]']
})
.then((row) => {
if (!row.enabled) {
// No need to add nginx config if host is disabled
return row;
}
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
row.meta = new_meta;
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
});
return internalNginx.configure(proxyHostModel, 'proxy_host', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
});
},
@@ -231,38 +213,41 @@ const internalProxyHost = {
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
if (typeof data === 'undefined') {
data = {};
}
return access
.can("proxy_hosts:get", thisData.id)
return access.can('proxy_hosts:get', data.id)
.then((access_data) => {
const query = proxyHostModel
let query = proxyHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,access_list.[clients,items],certificate]")
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[owner,access_list,access_list.[clients,items],certificate]')
.first();
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
// Custom omissions
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) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
if (row) {
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}
*/
delete: (access, data) => {
return access
.can("proxy_hosts:delete", data.id)
return access.can('proxy_hosts:delete', data.id)
.then(() => {
return internalProxyHost.get(access, { id: data.id });
return internalProxyHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
return proxyHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
is_deleted: 1,
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('proxy_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'deleted',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -319,41 +304,39 @@ const internalProxyHost = {
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("proxy_hosts:update", data.id)
return access.can('proxy_hosts:update', data.id)
.then(() => {
return internalProxyHost.get(access, {
id: data.id,
expand: ["certificate", "owner", "access_list"],
id: data.id,
expand: ['certificate', 'owner', 'access_list']
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
return proxyHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 1,
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(proxyHostModel, "proxy_host", row);
return internalNginx.configure(proxyHostModel, 'proxy_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'enabled',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -370,40 +353,39 @@ const internalProxyHost = {
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("proxy_hosts:update", data.id)
return access.can('proxy_hosts:update', data.id)
.then(() => {
return internalProxyHost.get(access, { id: data.id });
return internalProxyHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
return proxyHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 0,
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("proxy_host", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('proxy_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "proxy-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'disabled',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -420,38 +402,41 @@ const internalProxyHost = {
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, searchQuery) => {
const accessData = await access.can("proxy_hosts:list");
getAll: (access, expand, search_query) => {
return access.can('proxy_hosts:list')
.then((access_data) => {
let query = proxyHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[owner,access_list,certificate]')
.orderBy('domain_names', 'ASC');
const query = proxyHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,access_list,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (accessData.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%');
});
}
// Query is used for searching
if (typeof searchQuery === "string" && searchQuery.length > 0) {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${searchQuery}%`);
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) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
},
/**
@@ -462,16 +447,20 @@ const internalProxyHost = {
* @returns {Promise}
*/
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") {
query.andWhere("owner_user_id", user_id);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
};
export default internalProxyHost;
module.exports = internalProxyHost;

View File

@@ -1,105 +1,98 @@
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import redirectionHostModel from "../models/redirection_host.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const _ = require('lodash');
const error = require('../lib/error');
const redirectionHostModel = require('../models/redirection_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
const omissions = () => {
return ["is_deleted"];
};
function omissions () {
return ['is_deleted'];
}
const internalRedirectionHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
let create_certificate = data.certificate_id === 'new';
if (createCertificate) {
delete thisData.certificate_id;
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("redirection_hosts:create", thisData)
return access.can('redirection_hosts:create', data)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
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));
return true;
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
});
})
.then(() => {
// At this point the domains should have been checked
thisData.owner_user_id = access.token.getUserId(1);
thisData = internalHost.cleanSslHstsData(thisData);
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
// Fix for db field not having a default value
// for this optional field.
if (typeof data.advanced_config === "undefined") {
data.advanced_config = "";
}
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
return redirectionHostModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((row) => {
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, thisData)
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalRedirectionHost.update(access, {
id: row.id,
certificate_id: cert.id,
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalRedirectionHost.get(access, {
id: row.id,
expand: ["certificate", "owner"],
id: row.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row).then(() => {
return row;
});
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
.then(() => {
return row;
});
})
.then((row) => {
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
return internalAuditLog.add(access, {
action: 'created',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@@ -113,107 +106,94 @@ const internalRedirectionHost = {
* @return {Promise}
*/
update: (access, data) => {
let thisData = data || {};
const createCertificate = thisData.certificate_id === "new";
let create_certificate = data.certificate_id === 'new';
if (createCertificate) {
delete thisData.certificate_id;
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("redirection_hosts:update", thisData.id)
return access.can('redirection_hosts:update', data.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
const domain_name_check_promises = [];
let domain_name_check_promises = [];
if (typeof thisData.domain_names !== "undefined") {
thisData.domain_names.map((domain_name) => {
domain_name_check_promises.push(
internalHost.isHostnameTaken(domain_name, "redirection", thisData.id),
);
return true;
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'redirection', data.id));
});
return Promise.all(domain_name_check_promises).then((check_results) => {
check_results.map((result) => {
if (result.is_taken) {
throw new errs.ValidationError(`${result.hostname} is already in use`);
}
return true;
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
});
}
})
.then(() => {
return internalRedirectionHost.get(access, { id: thisData.id });
return internalRedirectionHost.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== thisData.id) {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Redirection Host could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
throw new error.InternalValidationError('Redirection Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (createCertificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: thisData.domain_names || row.domain_names,
meta: _.assign({}, row.meta, thisData.meta),
})
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
thisData.certificate_id = cert.id;
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
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,
);
data = _.assign({}, {
domain_names: row.domain_names
}, data);
thisData = internalHost.cleanSslHstsData(thisData, row);
data = internalHost.cleanSslHstsData(data, row);
return redirectionHostModel
.query()
.where({ id: thisData.id })
.patch(thisData)
.where({id: data.id})
.patch(data)
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "redirection-host",
object_id: row.id,
meta: thisData,
})
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalRedirectionHost
.get(access, {
id: thisData.id,
expand: ["owner", "certificate"],
})
return internalRedirectionHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
.then((row) => {
// Configure nginx
return internalNginx
.configure(redirectionHostModel, "redirection_host", row)
return internalNginx.configure(redirectionHostModel, 'redirection_host', row)
.then((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}
*/
get: (access, data) => {
const thisData = data || {};
if (typeof data === 'undefined') {
data = {};
}
return access
.can("redirection_hosts:get", thisData.id)
return access.can('redirection_hosts:get', data.id)
.then((access_data) => {
const query = redirectionHostModel
let query = redirectionHostModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,certificate]")
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[owner,certificate]')
.first();
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
// Custom omissions
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) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
throw new errs.ItemNotFoundError(thisData.id);
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
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}
*/
delete: (access, data) => {
return access
.can("redirection_hosts:delete", data.id)
return access.can('redirection_hosts:delete', data.id)
.then(() => {
return internalRedirectionHost.get(access, { id: data.id });
return internalRedirectionHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
return redirectionHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
is_deleted: 1,
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('redirection_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'deleted',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -317,41 +299,39 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("redirection_hosts:update", data.id)
return access.can('redirection_hosts:update', data.id)
.then(() => {
return internalRedirectionHost.get(access, {
id: data.id,
expand: ["certificate", "owner"],
id: data.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Host is already enabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
return redirectionHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 1,
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(redirectionHostModel, "redirection_host", row);
return internalNginx.configure(redirectionHostModel, 'redirection_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'enabled',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -368,40 +348,39 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("redirection_hosts:update", data.id)
return access.can('redirection_hosts:update', data.id)
.then(() => {
return internalRedirectionHost.get(access, { id: data.id });
return internalRedirectionHost.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Host is already disabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
return redirectionHostModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 0,
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("redirection_host", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('redirection_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "redirection-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'disabled',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -419,35 +398,35 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access
.can("redirection_hosts:list")
return access.can('redirection_hosts:list')
.then((access_data) => {
const query = redirectionHostModel
let query = redirectionHostModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.orderBy(castJsonIfNeed("domain_names"), "ASC");
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[owner,certificate]')
.orderBy('domain_names', 'ASC');
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === "string" && search_query.length > 0) {
if (typeof search_query === 'string') {
query.where(function () {
this.where(castJsonIfNeed("domain_names"), "like", `%${search_query}%`);
this.where('domain_names', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
return query;
})
.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);
}
@@ -463,16 +442,20 @@ const internalRedirectionHost = {
* @returns {Promise}
*/
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") {
query.andWhere("owner_user_id", user_id);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
return query.first()
.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";
import internalProxyHost from "./proxy-host.js";
import internalRedirectionHost from "./redirection-host.js";
import internalStream from "./stream.js";
const internalProxyHost = require('./proxy-host');
const internalRedirectionHost = require('./redirection-host');
const internalDeadHost = require('./dead-host');
const internalStream = require('./stream');
const internalReport = {
/**
* @param {Access} access
* @return {Promise}
*/
getHostsReport: (access) => {
return access
.can("reports:hosts", 1)
return access.can('reports:hosts', 1)
.then((access_data) => {
const userId = access.token.getUserId(1);
let user_id = access.token.getUserId(1);
const promises = [
internalProxyHost.getCount(userId, access_data.visibility),
internalRedirectionHost.getCount(userId, access_data.visibility),
internalStream.getCount(userId, access_data.visibility),
internalDeadHost.getCount(userId, access_data.visibility),
let promises = [
internalProxyHost.getCount(user_id, access_data.visibility),
internalRedirectionHost.getCount(user_id, access_data.visibility),
internalStream.getCount(user_id, access_data.visibility),
internalDeadHost.getCount(user_id, access_data.visibility)
];
return Promise.all(promises);
})
.then((counts) => {
return {
proxy: counts.shift(),
proxy: counts.shift(),
redirection: counts.shift(),
stream: counts.shift(),
dead: counts.shift(),
stream: counts.shift(),
dead: counts.shift()
};
});
},
}
};
export default internalReport;
module.exports = internalReport;

View File

@@ -1,9 +1,10 @@
import fs from "node:fs";
import errs from "../lib/error.js";
import settingModel from "../models/setting.js";
import internalNginx from "./nginx.js";
const fs = require('fs');
const error = require('../lib/error');
const settingModel = require('../models/setting');
const internalNginx = require('./nginx');
const internalSetting = {
/**
* @param {Access} access
* @param {Object} data
@@ -11,38 +12,37 @@ const internalSetting = {
* @return {Promise}
*/
update: (access, data) => {
return access
.can("settings:update", data.id)
return access.can('settings:update', data.id)
.then((/*access_data*/) => {
return internalSetting.get(access, { id: data.id });
return internalSetting.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Setting could not be updated, IDs do not match: ${row.id} !== ${data.id}`,
);
throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
return settingModel.query().where({ id: data.id }).patch(data);
return settingModel
.query()
.where({id: data.id})
.patch(data);
})
.then(() => {
return internalSetting.get(access, {
id: data.id,
id: data.id
});
})
.then((row) => {
if (row.id === "default-site") {
if (row.id === 'default-site') {
// write the html if we need to
if (row.value === "html") {
fs.writeFileSync("/data/nginx/default_www/index.html", row.meta.html, { encoding: "utf8" });
if (row.value === 'html') {
fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
}
// Configure nginx
return internalNginx
.deleteConfig("default")
return internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.generateConfig("default", row);
return internalNginx.generateConfig('default', row);
})
.then(() => {
return internalNginx.test();
@@ -54,8 +54,7 @@ const internalSetting = {
return row;
})
.catch((/*err*/) => {
internalNginx
.deleteConfig("default")
internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.test();
})
@@ -64,11 +63,12 @@ const internalSetting = {
})
.then(() => {
// 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}
*/
get: (access, data) => {
return access
.can("settings:get", data.id)
return access.can('settings:get', data.id)
.then(() => {
return settingModel.query().where("id", data.id).first();
return settingModel
.query()
.where('id', data.id)
.first();
})
.then((row) => {
if (row) {
return row;
} else {
throw new error.ItemNotFoundError(data.id);
}
throw new errs.ItemNotFoundError(data.id);
});
},
@@ -99,13 +102,15 @@ const internalSetting = {
* @returns {*}
*/
getCount: (access) => {
return access
.can("settings:list")
return access.can('settings:list')
.then(() => {
return settingModel.query().count("id as count").first();
return settingModel
.query()
.count('id as count')
.first();
})
.then((row) => {
return Number.parseInt(row.count, 10);
return parseInt(row.count, 10);
});
},
@@ -116,10 +121,13 @@ const internalSetting = {
* @returns {Promise}
*/
getAll: (access) => {
return access.can("settings:list").then(() => {
return settingModel.query().orderBy("description", "ASC");
});
},
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.orderBy('description', 'ASC');
});
}
};
export default internalSetting;
module.exports = internalSetting;

View File

@@ -1,85 +1,50 @@
import _ from "lodash";
import errs from "../lib/error.js";
import { castJsonIfNeed } from "../lib/helpers.js";
import utils from "../lib/utils.js";
import streamModel from "../models/stream.js";
import internalAuditLog from "./audit-log.js";
import internalCertificate from "./certificate.js";
import internalHost from "./host.js";
import internalNginx from "./nginx.js";
const _ = require('lodash');
const error = require('../lib/error');
const streamModel = require('../models/stream');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const omissions = () => {
return ["is_deleted", "owner.is_deleted", "certificate.is_deleted"];
};
function omissions () {
return ['is_deleted'];
}
const internalStream = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
const create_certificate = data.certificate_id === "new";
if (create_certificate) {
delete data.certificate_id;
}
return access
.can("streams:create", data)
return access.can('streams:create', data)
.then((/*access_data*/) => {
// TODO: At this point the existing ports should have been checked
data.owner_user_id = access.token.getUserId(1);
if (typeof data.meta === "undefined") {
if (typeof data.meta === 'undefined') {
data.meta = {};
}
// streams aren't routed by domain name so don't store domain names in the DB
const data_no_domains = structuredClone(data);
delete data_no_domains.domain_names;
return streamModel.query().insertAndFetch(data_no_domains).then(utils.omitRow(omissions()));
})
.then((row) => {
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalStream.update(access, {
id: row.id,
certificate_id: cert.id,
});
})
.then(() => {
return row;
});
}
return row;
})
.then((row) => {
// re-fetch with cert
return internalStream.get(access, {
id: row.id,
expand: ["certificate", "owner"],
});
return streamModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((row) => {
// Configure nginx
return internalNginx.configure(streamModel, "stream", row).then(() => {
return row;
});
return internalNginx.configure(streamModel, 'stream', row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "created",
object_type: "stream",
object_id: row.id,
meta: data,
})
return internalAuditLog.add(access, {
action: 'created',
object_type: 'stream',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
@@ -93,78 +58,39 @@ const internalStream = {
* @return {Promise}
*/
update: (access, data) => {
let thisData = data;
const create_certificate = thisData.certificate_id === "new";
if (create_certificate) {
delete thisData.certificate_id;
}
return access
.can("streams:update", thisData.id)
return access.can('streams:update', data.id)
.then((/*access_data*/) => {
// TODO: at this point the existing streams should have been checked
return internalStream.get(access, { id: thisData.id });
return internalStream.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== thisData.id) {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`Stream could not be updated, IDs do not match: ${row.id} !== ${thisData.id}`,
);
throw new error.InternalValidationError('Stream could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate
.createQuickCertificate(access, {
domain_names: 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
.query()
.patchAndFetchById(row.id, thisData)
.then(utils.omitRow(omissions()))
.omit(omissions())
.patchAndFetchById(row.id, data)
.then((saved_row) => {
return internalNginx.configure(streamModel, 'stream', saved_row)
.then(() => {
return internalStream.get(access, {id: row.id, expand: ['owner']});
});
})
.then((saved_row) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "stream",
object_id: row.id,
meta: thisData,
})
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'stream',
object_id: row.id,
meta: data
})
.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}
*/
get: (access, data) => {
const thisData = data || {};
if (typeof data === 'undefined') {
data = {};
}
return access
.can("streams:get", thisData.id)
return access.can('streams:get', data.id)
.then((access_data) => {
const query = streamModel
let query = streamModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[owner,certificate]")
.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));
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
// Custom omissions
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) => {
let thisRow = row;
if (!thisRow || !thisRow.id) {
throw new errs.ItemNotFoundError(thisData.id);
if (row) {
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;
});
},
@@ -221,35 +148,35 @@ const internalStream = {
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("streams:delete", data.id)
return access.can('streams:delete', data.id)
.then(() => {
return internalStream.get(access, { id: data.id });
return internalStream.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
return streamModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
is_deleted: 1,
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('stream', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'deleted',
object_type: 'stream',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -266,41 +193,39 @@ const internalStream = {
* @returns {Promise}
*/
enable: (access, data) => {
return access
.can("streams:update", data.id)
return access.can('streams:update', data.id)
.then(() => {
return internalStream.get(access, {
id: data.id,
expand: ["certificate", "owner"],
id: data.id,
expand: ['owner']
});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (row.enabled) {
throw new errs.ValidationError("Stream is already enabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
return streamModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 1,
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(streamModel, "stream", row);
return internalNginx.configure(streamModel, 'stream', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "enabled",
object_type: "stream",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'enabled',
object_type: 'stream',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -317,40 +242,39 @@ const internalStream = {
* @returns {Promise}
*/
disable: (access, data) => {
return access
.can("streams:update", data.id)
return access.can('streams:update', data.id)
.then(() => {
return internalStream.get(access, { id: data.id });
return internalStream.get(access, {id: data.id});
})
.then((row) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(data.id);
}
if (!row.enabled) {
throw new errs.ValidationError("Stream is already disabled");
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
return streamModel
.query()
.where("id", row.id)
.where('id', row.id)
.patch({
enabled: 0,
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig("stream", row).then(() => {
return internalNginx.reload();
});
return internalNginx.deleteConfig('stream', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "disabled",
object_type: "stream-host",
object_id: row.id,
meta: _.omit(row, omissions()),
action: 'disabled',
object_type: 'stream-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
@@ -368,39 +292,32 @@ const internalStream = {
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access
.can("streams:list")
return access.can('streams:list')
.then((access_data) => {
const query = streamModel
let query = streamModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[owner,certificate]")
.orderBy("incoming_port", "ASC");
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[owner]')
.orderBy('incoming_port', 'ASC');
if (access_data.permission_visibility !== "all") {
query.andWhere("owner_user_id", access.token.getUserId(1));
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === "string" && search_query.length > 0) {
if (typeof search_query === 'string') {
query.where(function () {
this.where(castJsonIfNeed("incoming_port"), "like", `%${search_query}%`);
this.where('incoming_port', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query.then(utils.omitRows(omissions()));
})
.then((rows) => {
if (typeof expand !== "undefined" && expand !== null && expand.indexOf("certificate") !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
return query;
});
},
@@ -412,16 +329,20 @@ const internalStream = {
* @returns {Promise}
*/
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") {
query.andWhere("owner_user_id", user_id);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first().then((row) => {
return Number.parseInt(row.count, 10);
});
},
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
};
export default internalStream;
module.exports = internalStream;

View File

@@ -1,14 +1,12 @@
import _ from "lodash";
import errs from "../lib/error.js";
import { parseDatePeriod } from "../lib/helpers.js";
import authModel from "../models/auth.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
const _ = require('lodash');
const error = require('../lib/error');
const userModel = require('../models/user');
const authModel = require('../models/auth');
const helpers = require('../lib/helpers');
const TokenModel = require('../models/token');
const ERROR_MESSAGE_INVALID_AUTH = "Invalid email or password";
const ERROR_MESSAGE_INVALID_AUTH_I18N = "error.invalid-auth";
module.exports = {
export default {
/**
* @param {Object} data
* @param {String} data.identity
@@ -18,66 +16,70 @@ export default {
* @param {String} [issuer]
* @returns {Promise}
*/
getTokenFromEmail: async (data, issuer) => {
const Token = TokenModel();
getTokenFromEmail: (data, issuer) => {
let Token = new TokenModel();
data.scope = data.scope || "user";
data.expiry = data.expiry || "1d";
data.scope = data.scope || 'user';
data.expiry = data.expiry || '1d';
const user = await userModel
return userModel
.query()
.where("email", data.identity.toLowerCase().trim())
.andWhere("is_deleted", 0)
.andWhere("is_disabled", 0)
.first();
.where('email', data.identity)
.andWhere('is_deleted', 0)
.andWhere('is_disabled', 0)
.first()
.then((user) => {
if (user) {
// Get auth
return authModel
.query()
.where('user_id', '=', user.id)
.where('type', '=', 'password')
.first()
.then((auth) => {
if (auth) {
return auth.verifyPassword(data.secret)
.then((valid) => {
if (valid) {
if (!user) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
const auth = await authModel
.query()
.where("user_id", "=", user.id)
.where("type", "=", "password")
.first();
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
if (!auth) {
throw new errs.AuthError(ERROR_MESSAGE_INVALID_AUTH);
}
const valid = await auth.verifyPassword(data.secret);
if (!valid) {
throw new errs.AuthError(
ERROR_MESSAGE_INVALID_AUTH,
ERROR_MESSAGE_INVALID_AUTH_I18N,
);
}
if (data.scope !== "user" && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new errs.AuthError(`Invalid scope: ${data.scope}`);
}
// Create a moment of the expiry expression
const expiry = parseDatePeriod(data.expiry);
if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${data.expiry}`);
}
const signed = await Token.create({
iss: issuer || "api",
attrs: {
id: user.id,
},
scope: [data.scope],
expiresIn: data.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
} else {
throw new error.AuthError('Invalid password');
}
});
} else {
throw new error.AuthError('No password auth for user');
}
});
} else {
throw new error.AuthError('No relevant user found');
}
});
},
/**
@@ -87,70 +89,74 @@ export default {
* @param {String} [data.scope] Only considered if existing token scope is admin
* @returns {Promise}
*/
getFreshToken: async (access, data) => {
const Token = TokenModel();
const thisData = data || {};
getFreshToken: (access, data) => {
let Token = new TokenModel();
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
const expiry = parseDatePeriod(thisData.expiry);
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new errs.AuthError(`Invalid expiry time: ${thisData.expiry}`);
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
const token_attrs = {
id: access.token.getUserId(0),
let token_attrs = {
id: access.token.getUserId(0)
};
// Only admins can request otherwise scoped tokens
let scope = access.token.get("scope");
if (thisData.scope && access.token.hasScope("admin")) {
scope = [thisData.scope];
let scope = access.token.get('scope');
if (data.scope && access.token.hasScope('admin')) {
scope = [data.scope];
if (thisData.scope === "job-board" || thisData.scope === "worker") {
if (data.scope === 'job-board' || data.scope === 'worker') {
token_attrs.id = 0;
}
}
const signed = await Token.create({
iss: "api",
scope: scope,
attrs: token_attrs,
expiresIn: thisData.expiry,
});
return {
token: signed.token,
expires: expiry.toISOString(),
};
return Token.create({
iss: 'api',
scope: scope,
attrs: token_attrs,
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
} else {
throw new error.AssertionFailedError('Existing token contained invalid user data');
}
throw new error.AssertionFailedError("Existing token contained invalid user data");
},
/**
* @param {Object} user
* @returns {Promise}
*/
getTokenFromUser: async (user) => {
const expire = "1d";
const Token = TokenModel();
const expiry = parseDatePeriod(expire);
getTokenFromUser: (user) => {
const expire = '1d';
const Token = new TokenModel();
const expiry = helpers.parseDatePeriod(expire);
const signed = await Token.create({
iss: "api",
return Token.create({
iss: 'api',
attrs: {
id: user.id,
id: user.id
},
scope: ["user"],
expiresIn: expire,
});
return {
token: signed.token,
expires: expiry.toISOString(),
user: user,
};
},
scope: ['user'],
expiresIn: expire
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString(),
user: user
};
});
}
};

View File

@@ -1,76 +1,92 @@
import gravatar from "gravatar";
import _ from "lodash";
import errs from "../lib/error.js";
import utils from "../lib/utils.js";
import authModel from "../models/auth.js";
import userModel from "../models/user.js";
import userPermissionModel from "../models/user_permission.js";
import internalAuditLog from "./audit-log.js";
import internalToken from "./token.js";
const _ = require('lodash');
const error = require('../lib/error');
const userModel = require('../models/user');
const userPermissionModel = require('../models/user_permission');
const authModel = require('../models/auth');
const gravatar = require('gravatar');
const internalToken = require('./token');
const internalAuditLog = require('./audit-log');
const omissions = () => {
return ["is_deleted", "permissions.id", "permissions.user_id", "permissions.created_on", "permissions.modified_on"];
};
const DEFAULT_AVATAR = gravatar.url("admin@example.com", { default: "mm" });
function omissions () {
return ['is_deleted'];
}
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 {Object} data
* @returns {Promise}
*/
create: async (access, data) => {
const auth = data.auth || null;
create: (access, data) => {
let auth = data.auth || null;
delete data.auth;
data.avatar = data.avatar || "";
data.roles = data.roles || [];
data.avatar = data.avatar || '';
data.roles = data.roles || [];
if (typeof data.is_disabled !== "undefined") {
if (typeof data.is_disabled !== 'undefined') {
data.is_disabled = data.is_disabled ? 1 : 0;
}
await access.can("users:create", data);
data.avatar = gravatar.url(data.email, { default: "mm" });
return access.can('users:create', data)
.then(() => {
data.avatar = gravatar.url(data.email, {default: 'mm'});
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
if (auth) {
user = await authModel.query().insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {},
return userModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((user) => {
if (auth) {
return authModel
.query()
.insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
meta: {}
})
.then(() => {
return user;
});
} else {
return user;
}
})
.then((user) => {
// Create permissions row as well
let is_admin = data.roles.indexOf('admin') !== -1;
return userPermissionModel
.query()
.insert({
user_id: user.id,
visibility: is_admin ? 'all' : 'user',
proxy_hosts: 'manage',
redirection_hosts: 'manage',
dead_hosts: 'manage',
streams: 'manage',
access_lists: 'manage',
certificates: 'manage'
})
.then(() => {
return internalUser.get(access, {id: user.id, expand: ['permissions']});
});
})
.then((user) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'user',
object_id: user.id,
meta: user
})
.then(() => {
return user;
});
});
}
// 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 +98,65 @@ const internalUser = {
* @return {Promise}
*/
update: (access, data) => {
if (typeof data.is_disabled !== "undefined") {
if (typeof data.is_disabled !== 'undefined') {
data.is_disabled = data.is_disabled ? 1 : 0;
}
return access
.can("users:update", data.id)
return access.can('users:update', data.id)
.then(() => {
// Make sure that the user being updated doesn't change their email to another user that is already using it
// 1. get user we want to update
return internalUser.get(access, { id: data.id }).then((user) => {
// 2. if email is to be changed, find other users with that email
if (typeof data.email !== "undefined") {
data.email = data.email.toLowerCase().trim();
return internalUser.get(access, {id: data.id})
.then((user) => {
if (user.email !== data.email) {
return internalUser.isEmailAvailable(data.email, data.id).then((available) => {
if (!available) {
throw new errs.ValidationError(`Email address already in use - ${data.email}`);
}
return user;
});
// 2. if email is to be changed, find other users with that email
if (typeof data.email !== 'undefined') {
data.email = data.email.toLowerCase().trim();
if (user.email !== data.email) {
return internalUser.isEmailAvailable(data.email, data.id)
.then((available) => {
if (!available) {
throw new error.ValidationError('Email address already in use - ' + data.email);
}
return user;
});
}
}
}
// No change to email:
return user;
});
// No change to email:
return user;
});
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
}
data.avatar = gravatar.url(data.email || user.email, { default: "mm" });
return userModel.query().patchAndFetchById(user.id, data).then(utils.omitRow(omissions()));
data.avatar = gravatar.url(data.email || user.email, {default: 'mm'});
return userModel
.query()
.omit(omissions())
.patchAndFetchById(user.id, data)
.then((saved_user) => {
return _.omit(saved_user, omissions());
});
})
.then(() => {
return internalUser.get(access, { id: data.id });
return internalUser.get(access, {id: data.id});
})
.then((user) => {
// Add to audit log
return internalAuditLog
.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: data,
})
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: data
})
.then(() => {
return user;
});
@@ -148,42 +172,40 @@ const internalUser = {
* @return {Promise}
*/
get: (access, data) => {
const thisData = data || {};
if (typeof thisData.id === "undefined" || !thisData.id) {
thisData.id = access.token.getUserId(0);
if (typeof data === 'undefined') {
data = {};
}
return access
.can("users:get", thisData.id)
if (typeof data.id === 'undefined' || !data.id) {
data.id = access.token.getUserId(0);
}
return access.can('users:get', data.id)
.then(() => {
const query = userModel
let query = userModel
.query()
.where("is_deleted", 0)
.andWhere("id", thisData.id)
.allowGraph("[permissions]")
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[permissions]')
.first();
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
query.withGraphFetched(`[${thisData.expand.join(", ")}]`);
// Custom omissions
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) => {
if (!row || !row.id) {
throw new errs.ItemNotFoundError(thisData.id);
if (row) {
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 +217,20 @@ const internalUser = {
* @param 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") {
query.where("id", "!=", user_id);
if (typeof user_id !== 'undefined') {
query.where('id', '!=', user_id);
}
return query.then((user) => {
return !user;
});
return query
.then((user) => {
return !user;
});
},
/**
@@ -214,34 +241,33 @@ const internalUser = {
* @returns {Promise}
*/
delete: (access, data) => {
return access
.can("users:delete", data.id)
return access.can('users:delete', data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
return internalUser.get(access, {id: data.id});
})
.then((user) => {
if (!user) {
throw new errs.ItemNotFoundError(data.id);
throw new error.ItemNotFoundError(data.id);
}
// Make sure user can't delete themselves
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
.query()
.where("id", user.id)
.where('id', user.id)
.patch({
is_deleted: 1,
is_deleted: 1
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: "deleted",
object_type: "user",
object_id: user.id,
meta: _.omit(user, omissions()),
action: 'deleted',
object_type: 'user',
object_id: user.id,
meta: _.omit(user, omissions())
});
});
})
@@ -250,14 +276,6 @@ const internalUser = {
});
},
deleteAll: async () => {
await userModel
.query()
.patch({
is_deleted: 1,
});
},
/**
* This will only count the users
*
@@ -266,26 +284,26 @@ const internalUser = {
* @returns {*}
*/
getCount: (access, search_query) => {
return access
.can("users:list")
return access.can('users:list')
.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
if (typeof search_query === "string") {
if (typeof search_query === 'string') {
query.where(function () {
this.where("user.name", "like", `%${search_query}%`).orWhere(
"user.email",
"like",
`%${search_query}%`,
);
this.where('user.name', 'like', '%' + search_query + '%')
.orWhere('user.email', 'like', '%' + search_query + '%');
});
}
return query;
})
.then((row) => {
return Number.parseInt(row.count, 10);
return parseInt(row.count, 10);
});
},
@@ -297,28 +315,31 @@ const internalUser = {
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: async (access, expand, search_query) => {
await access.can("users:list");
const query = userModel
.query()
.where("is_deleted", 0)
.groupBy("id")
.allowGraph("[permissions]")
.orderBy("name", "ASC");
getAll: (access, expand, search_query) => {
return access.can('users:list')
.then(() => {
let query = userModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[permissions]')
.orderBy('name', 'ASC');
// Query is used for searching
if (typeof search_query === "string") {
query.where(function () {
this.where("name", "like", `%${search_query}%`).orWhere("email", "like", `%${search_query}%`);
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('name', 'like', '%' + search_query + '%')
.orWhere('email', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
});
}
if (typeof expand !== "undefined" && expand !== null) {
query.withGraphFetched(`[${expand.join(", ")}]`);
}
const res = await query;
return utils.omitRows(omissions())(res);
},
/**
@@ -326,11 +347,11 @@ const internalUser = {
* @param {Integer} [id_requested]
* @returns {[String]}
*/
getUserOmisionsByAccess: (access, idRequested) => {
getUserOmisionsByAccess: (access, id_requested) => {
let response = []; // Admin response
if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) {
response = ["is_deleted"]; // Restricted response
if (!access.token.hasScope('admin') && access.token.getUserId(0) !== id_requested) {
response = ['roles', 'is_deleted']; // Restricted response
}
return response;
@@ -345,30 +366,26 @@ const internalUser = {
* @return {Promise}
*/
setPassword: (access, data) => {
return access
.can("users:password", data.id)
return access.can('users:password', data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
return internalUser.get(access, {id: data.id});
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
}
if (user.id === access.token.getUserId(0)) {
// they're setting their own password. Make sure their current password is correct
if (typeof data.current === "undefined" || !data.current) {
throw new errs.ValidationError("Current password was not supplied");
if (typeof data.current === 'undefined' || !data.current) {
throw new error.ValidationError('Current password was not supplied');
}
return internalToken
.getTokenFromEmail({
identity: user.email,
secret: data.current,
})
return internalToken.getTokenFromEmail({
identity: user.email,
secret: data.current
})
.then(() => {
return user;
});
@@ -380,36 +397,43 @@ const internalUser = {
// Get auth, patch if it exists
return authModel
.query()
.where("user_id", user.id)
.andWhere("type", data.type)
.where('user_id', user.id)
.andWhere('type', data.type)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
return authModel.query().where("user_id", user.id).andWhere("type", data.type).patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret,
});
return authModel
.query()
.where('user_id', user.id)
.andWhere('type', data.type)
.patch({
type: data.type, // This is required for the model to encrypt on save
secret: data.secret
});
} else {
// insert
return authModel
.query()
.insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {}
});
}
// insert
return authModel.query().insert({
user_id: user.id,
type: data.type,
secret: data.secret,
meta: {},
});
})
.then(() => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
password_changed: true,
auth_type: data.type,
},
auth_type: data.type
}
});
});
})
@@ -424,17 +448,14 @@ const internalUser = {
* @return {Promise}
*/
setPermissions: (access, data) => {
return access
.can("users:permissions", data.id)
return access.can('users:permissions', data.id)
.then(() => {
return internalUser.get(access, { id: data.id });
return internalUser.get(access, {id: data.id});
})
.then((user) => {
if (user.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
);
throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id);
}
return user;
@@ -443,30 +464,34 @@ const internalUser = {
// Get perms row, patch if it exists
return userPermissionModel
.query()
.where("user_id", user.id)
.where('user_id', user.id)
.first()
.then((existing_auth) => {
if (existing_auth) {
// patch
return userPermissionModel
.query()
.where("user_id", user.id)
.patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));
.where('user_id', user.id)
.patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data));
} else {
// insert
return userPermissionModel
.query()
.insertAndFetch(_.assign({user_id: user.id}, data));
}
// insert
return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
})
.then((permissions) => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: "updated",
object_type: "user",
object_id: user.id,
meta: {
name: user.name,
permissions: permissions,
},
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
permissions: permissions
}
});
});
})
.then(() => {
@@ -480,15 +505,14 @@ const internalUser = {
* @param {Integer} data.id
*/
loginAs: (access, data) => {
return access
.can("users:loginas", data.id)
return access.can('users:loginas', data.id)
.then(() => {
return internalUser.get(access, data);
})
.then((user) => {
return internalToken.getTokenFromUser(user);
});
},
}
};
export default internalUser;
module.exports = internalUser;

View File

@@ -1,6 +1,6 @@
module.exports = {
development: {
client: 'mysql2',
client: 'mysql',
migrations: {
tableName: 'migrations',
stub: 'lib/migrate_template.js',
@@ -9,7 +9,7 @@ module.exports = {
},
production: {
client: 'mysql2',
client: 'mysql',
migrations: {
tableName: 'migrations',
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
* 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.
*
*
*/
import fs from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import Ajv from "ajv/dist/2020.js";
import _ from "lodash";
import { access as logger } from "../logger.js";
import proxyHostModel from "../models/proxy_host.js";
import TokenModel from "../models/token.js";
import userModel from "../models/user.js";
import permsSchema from "./access/permissions.json" with { type: "json" };
import roleSchema from "./access/roles.json" with { type: "json" };
import errs from "./error.js";
const _ = require('lodash');
const logger = require('../logger').access;
const validator = require('ajv');
const error = require('./error');
const userModel = require('../models/user');
const proxyHostModel = require('../models/proxy_host');
const TokenModel = require('../models/token');
const roleSchema = require('./access/roles.json');
const permsSchema = require('./access/permissions.json');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default function (tokenString) {
const Token = TokenModel();
let tokenData = null;
let initialised = false;
const objectCache = {};
let allowInternalAccess = false;
let userRoles = [];
let permissions = {};
module.exports = function (token_string) {
let Token = new TokenModel();
let token_data = null;
let initialised = false;
let object_cache = {};
let allow_internal_access = false;
let user_roles = [];
let permissions = {};
/**
* Loads the Token object from the token string
*
* @returns {Promise}
*/
this.init = async () => {
if (initialised) {
return;
}
if (!tokenString) {
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;
this.init = () => {
return new Promise((resolve, reject) => {
if (initialised) {
resolve();
} else if (!token_string) {
reject(new error.PermissionError('Permission Denied'));
} 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
* by object scopes
*
* @param {String} objectType
* @param {String} object_type
* @returns {Promise}
*/
this.loadObjects = async (objectType) => {
let objects = null;
this.loadObjects = (object_type) => {
return new Promise((resolve, reject) => {
if (Token.hasScope('user')) {
if (typeof token_data.attrs.id === 'undefined' || !token_data.attrs.id) {
reject(new error.AuthError('User Token supplied without a User ID'));
} else {
let token_user_id = token_data.attrs.id ? token_data.attrs.id : 0;
let query;
if (Token.hasScope("user")) {
if (typeof tokenData.attrs.id === "undefined" || !tokenData.attrs.id) {
throw new errs.AuthError("User Token supplied without a User ID");
}
if (typeof object_cache[object_type] === 'undefined') {
switch (object_type) {
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") {
objects = objectCache[objectType];
} else {
switch (objectType) {
// USERS - should only return yourself
case "users":
objects = tokenUserId ? [tokenUserId] : [];
break;
// Proxy Hosts
case 'proxy_hosts':
query = proxyHostModel
.query()
.select('id')
.andWhere('is_deleted', 0);
// Proxy Hosts
case "proxy_hosts": {
const query = proxyHostModel
.query()
.select("id")
.andWhere("is_deleted", 0);
if (permissions.visibility === 'user') {
query.andWhere('owner_user_id', token_user_id);
}
if (permissions.visibility === "user") {
query.andWhere("owner_user_id", tokenUserId);
resolve(query
.then((rows) => {
let result = [];
_.forEach(rows, (rule_row) => {
result.push(rule_row.id);
});
// enum should not have less than 1 item
if (!result.length) {
result.push(0);
}
return result;
})
);
break;
// DEFAULT: null
default:
resolve(null);
break;
}
const rows = await query;
objects = [];
_.forEach(rows, (ruleRow) => {
result.push(ruleRow.id);
});
// enum should not have less than 1 item
if (!objects.length) {
objects.push(0);
}
break;
} else {
resolve(object_cache[object_type]);
}
}
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
*
* @param {String} permissionLabel
* @param {String} permission_label
* @returns {Object}
*/
this.getObjectSchema = async (permissionLabel) => {
const baseObjectType = permissionLabel.split(":").shift();
this.getObjectSchema = (permission_label) => {
let base_object_type = permission_label.split(':').shift();
const schema = {
$id: "objects",
description: "Actor Properties",
type: "object",
let schema = {
$id: 'objects',
$schema: 'http://json-schema.org/draft-07/schema#',
description: 'Actor Properties',
type: 'object',
additionalProperties: false,
properties: {
properties: {
user_id: {
anyOf: [
{
type: "number",
enum: [Token.get("attrs").id],
},
],
type: 'number',
enum: [Token.get('attrs').id]
}
]
},
scope: {
type: "string",
pattern: `^${Token.get("scope")}$`,
},
},
type: 'string',
pattern: '^' + Token.get('scope') + '$'
}
}
};
const result = await this.loadObjects(baseObjectType);
if (typeof result === "object" && result !== null) {
schema.properties[baseObjectType] = {
type: "number",
enum: result,
minimum: 1,
};
} else {
schema.properties[baseObjectType] = {
type: "number",
minimum: 1,
};
}
return this.loadObjects(base_object_type)
.then((object_result) => {
if (typeof object_result === 'object' && object_result !== null) {
schema.properties[base_object_type] = {
type: 'number',
enum: object_result,
minimum: 1
};
} else {
schema.properties[base_object_type] = {
type: 'number',
minimum: 1
};
}
return schema;
return schema;
});
};
// here:
return {
token: Token,
/**
*
* @param {Boolean} [allowInternal]
* @param {Boolean} [allow_internal]
* @returns {Promise}
*/
load: async (allowInternal) => {
if (tokenString) {
return await Token.load(tokenString);
}
allowInternalAccess = allowInternal;
return allowInternal || null;
load: (allow_internal) => {
return new Promise(function (resolve/*, reject*/) {
if (token_string) {
resolve(Token.load(token_string));
} else {
allow_internal_access = allow_internal;
resolve(allow_internal_access || null);
}
});
},
reloadObjects: this.loadObjects,
@@ -220,59 +241,74 @@ export default function (tokenString) {
* @param {*} [data]
* @returns {Promise}
*/
can: async (permission, data) => {
if (allowInternalAccess === true) {
return true;
can: (permission, data) => {
if (allow_internal_access === true) {
return Promise.resolve(true);
//return true;
} else {
return this.init()
.then(() => {
// Initialised, token decoded ok
return this.getObjectSchema(permission)
.then((objectSchema) => {
let data_schema = {
[permission]: {
data: data,
scope: Token.get('scope'),
roles: user_roles,
permission_visibility: permissions.visibility,
permission_proxy_hosts: permissions.proxy_hosts,
permission_redirection_hosts: permissions.redirection_hosts,
permission_dead_hosts: permissions.dead_hosts,
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates
}
};
let permissionSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
$async: true,
$id: 'permissions',
additionalProperties: false,
properties: {}
};
permissionSchema.properties[permission] = require('./access/' + permission.replace(/:/gim, '-') + '.json');
// logger.info('objectSchema', JSON.stringify(objectSchema, null, 2));
// logger.info('permissionSchema', JSON.stringify(permissionSchema, null, 2));
// logger.info('data_schema', JSON.stringify(data_schema, null, 2));
let ajv = validator({
verbose: true,
allErrors: true,
format: 'full',
missingRefs: 'fail',
breakOnError: true,
coerceTypes: true,
schemas: [
roleSchema,
permsSchema,
objectSchema,
permissionSchema
]
});
return ajv.validate('permissions', data_schema)
.then(() => {
return data_schema[permission];
});
});
})
.catch((err) => {
err.permission = permission;
err.permission_data = data;
logger.error(permission, data, err.message);
throw new error.PermissionError('Permission Denied', err);
});
}
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",
"definitions": {
"view": {

View File

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

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 = {
PermissionError: function (_, previous) {
module.exports = {
PermissionError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = "Permission Denied";
this.public = true;
this.status = 403;
this.message = 'Permission Denied';
this.public = true;
this.status = 403;
},
ItemNotFoundError: function (id, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = "Not Found";
if (id) {
this.message = `Not Found - ${id}`;
}
this.public = true;
this.status = 404;
this.message = 'Item Not Found - ' + id;
this.public = true;
this.status = 404;
},
AuthError: function (message, messageI18n, previous) {
AuthError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.message_i18n = messageI18n;
this.public = true;
this.status = 400;
this.message = message;
this.public = true;
this.status = 401;
},
InternalError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 500;
this.public = false;
this.message = message;
this.status = 500;
this.public = false;
},
InternalValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = false;
this.message = message;
this.status = 400;
this.public = false;
},
ConfigurationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = true;
this.message = message;
this.status = 400;
this.public = true;
},
CacheError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.name = this.constructor.name;
this.message = message;
this.previous = previous;
this.status = 500;
this.public = false;
this.status = 500;
this.public = false;
},
ValidationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = true;
this.status = 400;
this.message = message;
this.public = true;
this.status = 400;
},
AssertionFailedError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.public = false;
this.status = 400;
},
CommandError: function (stdErr, code, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = stdErr;
this.code = code;
this.public = false;
},
this.message = message;
this.public = false;
this.status = 400;
}
};
_.forEach(errs, (err) => {
err.prototype = Object.create(Error.prototype);
_.forEach(module.exports, function (error) {
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) {
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();
const originSchema = {
oneOf: [
{
type: 'string',
pattern: '^[a-z\\-]+:\\/\\/(?:[\\w\\-\\.]+(:[0-9]+)?/?)?$'
},
{
type: 'string',
pattern: '^[a-z\\-]+:\\/\\/(?:\\[([a-z0-9]{0,4}\\:?)+\\])?/?(:[0-9]+)?$'
}
]
};
// very relaxed validation....
validator(originSchema, req.headers.origin)
.then(function () {
res.set({
'Access-Control-Allow-Origin': req.headers.origin,
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires, Authorization, X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit',
'Access-Control-Max-Age': 5 * 60,
'Access-Control-Expose-Headers': 'X-Dataset-Total, X-Dataset-Offset, X-Dataset-Limit'
});
next();
})
.catch(next);
} else {
// No origin
next();
}
};

View File

@@ -1,15 +1,15 @@
import Access from "../access.js";
const Access = require('../access');
export default () => {
return async (_, res, next) => {
try {
res.locals.access = null;
const access = new Access(res.locals.token || null);
await access.load();
res.locals.access = access;
next();
} catch (err) {
next(err);
}
module.exports = () => {
return function (req, res, next) {
res.locals.access = null;
let access = new Access(res.locals.token || null);
access.load()
.then(() => {
res.locals.access = access;
next();
})
.catch(next);
};
};

View File

@@ -1,13 +1,13 @@
export default function () {
return (req, res, next) => {
module.exports = function () {
return function (req, res, next) {
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];
}
}
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
*
@@ -10,35 +11,34 @@ export default (default_sort, default_offset, default_limit, max_limit) => {
*
*/
return (req, _res, next) => {
req.query.offset =
typeof req.query.limit === "undefined" ? default_offset || 0 : Number.parseInt(req.query.offset, 10);
req.query.limit =
typeof req.query.limit === "undefined" ? default_limit || 50 : Number.parseInt(req.query.limit, 10);
return function (req, res, next) {
req.query.offset = typeof req.query.limit === 'undefined' ? default_offset || 0 : parseInt(req.query.offset, 10);
req.query.limit = typeof req.query.limit === 'undefined' ? default_limit || 50 : parseInt(req.query.limit, 10);
if (max_limit && req.query.limit > max_limit) {
req.query.limit = max_limit;
}
// Sorting
let sort = typeof req.query.sort === "undefined" ? default_sort : req.query.sort;
const myRegexp = /.*\.(asc|desc)$/gi;
const sort_array = [];
let sort = typeof req.query.sort === 'undefined' ? default_sort : req.query.sort;
let myRegexp = /.*\.(asc|desc)$/ig;
let sort_array = [];
sort = sort.split(",");
_.map(sort, (val) => {
const matches = myRegexp.exec(val);
sort = sort.split(',');
_.map(sort, function (val) {
let matches = myRegexp.exec(val);
if (matches !== null) {
const dir = matches[1];
let dir = matches[1];
sort_array.push({
field: val.substr(0, val.length - (dir.length + 1)),
dir: dir.toLowerCase(),
dir: dir.toLowerCase()
});
} else {
sort_array.push({
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) {
req.params.user_id = res.locals.access.token.get('attrs').id;
} else {
req.params.user_id = Number.parseInt(req.params.user_id, 10);
req.params.user_id = parseInt(req.params.user_id, 10);
}
next();
};

View File

@@ -1,58 +1,32 @@
import moment from "moment";
import { ref } from "objection";
import { isPostgres } from "./config.js";
const moment = require('moment');
/**
* Takes an expression such as 30d and returns a moment object of that date in future
*
* Key Shorthand
* ==================
* years y
* quarters Q
* months M
* weeks w
* days d
* hours h
* minutes m
* seconds s
* milliseconds ms
*
* @param {String} expression
* @returns {Object}
*/
const parseDatePeriod = (expression) => {
const matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
module.exports = {
/**
* Takes an expression such as 30d and returns a moment object of that date in future
*
* Key Shorthand
* ==================
* years y
* quarters Q
* months M
* weeks w
* days d
* hours h
* minutes m
* seconds s
* milliseconds ms
*
* @param {String} expression
* @returns {Object}
*/
parseDatePeriod: function (expression) {
let matches = expression.match(/^([0-9]+)(y|Q|M|w|d|h|m|s|ms)$/m);
if (matches) {
return moment().add(matches[1], matches[2]);
}
return null;
}
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 migrateName = "identifier_for_migrate";
const migrate_name = 'identifier_for_migrate';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const up = (_knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
exports.up = function (knex, Promise) {
logger.info('[' + migrate_name + '] Migrating Up...');
// Create Table example:
/*
return knex.schema.createTable('notification', (table) => {
/*return knex.schema.createTable('notification', (table) => {
table.increments().primary();
table.string('name').notNull();
table.string('type').notNull();
table.integer('created_on').notNull();
table.integer('modified_on').notNull();
})
.then(function () {
logger.info('[' + migrateName + '] Notification Table created');
});
*/
.then(function () {
logger.info('[' + migrate_name + '] Notification Table created');
});*/
logger.info(`[${migrateName}] Migrating Up Complete`);
logger.info('[' + migrate_name + '] Migrating Up Complete');
return Promise.resolve(true);
};
@@ -36,24 +35,21 @@ const up = (_knex) => {
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.info(`[${migrateName}] Migrating Down...`);
exports.down = function (knex, Promise) {
logger.info('[' + migrate_name + '] Migrating Down...');
// Drop table example:
/*
return knex.schema.dropTable('notification')
.then(() => {
logger.info(`[${migrateName}] Notification Table dropped`);
});
*/
/*return knex.schema.dropTable('notification')
.then(() => {
logger.info('[' + migrate_name + '] Notification Table dropped');
});*/
logger.info(`[${migrateName}] Migrating Down Complete`);
logger.info('[' + migrate_name + '] Migrating Down Complete');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,110 +1,20 @@
import { exec as nodeExec, execFile as nodeExecFile } from "node:child_process";
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 exec = require('child_process').exec;
const __filename = fileURLToPath(import.meta.url);
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/`,
});
module.exports = {
/**
* nginxAccessRule expects the object given to have 2 properties:
*
* directive string
* address string
* @param {String} cmd
* @returns {Promise}
*/
renderEngine.registerFilter("nginxAccessRule", (v) => {
if (typeof v.directive !== "undefined" && typeof v.address !== "undefined" && v.directive && v.address) {
return `${v.directive} ${v.address};`;
}
return "";
});
return renderEngine;
exec: function (cmd) {
return new Promise((resolve, reject) => {
exec(cmd, function (err, stdout, /*stderr*/) {
if (err && typeof err === 'object') {
reject(err);
} else {
resolve(stdout.trim());
}
});
});
}
};
export default { exec, execFile, omitRow, omitRows, getRenderEngine };

View File

@@ -1,12 +1,13 @@
import Ajv from "ajv/dist/2020.js";
import errs from "../error.js";
const error = require('../error');
const path = require('path');
const parser = require('json-schema-ref-parser');
const ajv = new Ajv({
verbose: true,
allErrors: true,
allowUnionTypes: true,
strict: false,
coerceTypes: true,
const ajv = require('ajv')({
verbose: true,
validateSchema: true,
allErrors: false,
format: 'full',
coerceTypes: true
});
/**
@@ -14,27 +15,31 @@ const ajv = new Ajv({
* @param {Object} payload
* @returns {Promise}
*/
const apiValidator = async (schema, payload /*, description*/) => {
if (!schema) {
throw new errs.ValidationError("Schema is undefined");
}
function apiValidator (schema, payload/*, description*/) {
return new Promise(function Promise_apiValidator (resolve, reject) {
if (typeof payload === 'undefined') {
reject(new error.ValidationError('Payload is undefined'));
}
// Can't use falsy check here as valid payload could be `0` or `false`
if (typeof payload === "undefined") {
throw new errs.ValidationError("Payload is undefined");
}
let validate = ajv.compile(schema);
let valid = validate(payload);
const validate = ajv.compile(schema);
const valid = validate(payload);
if (valid && !validate.errors) {
resolve(payload);
} else {
let message = ajv.errorsText(validate.errors);
let err = new error.ValidationError(message);
err.debug = [validate.errors, payload];
reject(err);
}
});
}
if (valid && !validate.errors) {
return payload;
}
apiValidator.loadSchemas = parser
.dereference(path.resolve('schema/index.json'))
.then((schema) => {
ajv.addSchema(schema);
return schema;
});
const message = ajv.errorsText(validate.errors);
const err = new errs.ValidationError(message);
err.debug = [validate.errors, payload];
throw err;
};
export default apiValidator;
module.exports = apiValidator;

View File

@@ -1,17 +1,17 @@
import Ajv from 'ajv/dist/2020.js';
import _ from "lodash";
import commonDefinitions from "../../schema/common.json" with { type: "json" };
import errs from "../error.js";
const _ = require('lodash');
const error = require('../error');
const definitions = require('../../schema/definitions.json');
RegExp.prototype.toJSON = RegExp.prototype.toString;
const ajv = new Ajv({
verbose: true,
allErrors: true,
allowUnionTypes: true,
const ajv = require('ajv')({
verbose: true, //process.env.NODE_ENV === 'development',
allErrors: true,
format: 'full', // strict regexes for format checks
coerceTypes: true,
strict: false,
schemas: [commonDefinitions],
schemas: [
definitions
]
});
/**
@@ -20,26 +20,30 @@ const ajv = new Ajv({
* @param {Object} payload
* @returns {Promise}
*/
const validator = (schema, payload) => {
return new Promise((resolve, reject) => {
function validator (schema, payload) {
return new Promise(function (resolve, reject) {
if (!payload) {
reject(new errs.InternalValidationError("Payload is falsy"));
reject(new error.InternalValidationError('Payload is falsy'));
} else {
try {
const validate = ajv.compile(schema);
const valid = validate(payload);
let validate = ajv.compile(schema);
let valid = validate(payload);
if (valid && !validate.errors) {
resolve(_.cloneDeep(payload));
} else {
const message = ajv.errorsText(validate.errors);
reject(new errs.InternalValidationError(message));
let message = ajv.errorsText(validate.errors);
reject(new error.InternalValidationError(message));
}
} catch (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 = {
logLevel: "info",
module.exports = {
global: new Signale({scope: 'Global '}),
migrate: new Signale({scope: 'Migrate '}),
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '}),
import: new Signale({scope: 'Importer '}),
setup: new Signale({scope: 'Setup '}),
ip_ranges: new Signale({scope: 'IP Ranges'})
};
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";
import { migrate as logger } from "./logger.js";
const db = require('./db');
const logger = require('./logger').migrate;
const migrateUp = async () => {
const version = await db.migrate.currentVersion();
logger.info("Current database version:", version);
return await db.migrate.latest({
tableName: "migrations",
directory: "migrations",
});
module.exports = {
latest: function () {
return db.migrate.currentVersion()
.then((version) => {
logger.info('Current database version:', version);
return db.migrate.latest({
tableName: 'migrations',
directory: 'migrations'
});
});
}
};
export { migrateUp };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "initial-schema";
const migrate_name = 'initial-schema';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,199 +7,202 @@ const migrateName = "initial-schema";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.createTable("auth", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("type", 30).notNull();
table.string("secret").notNull();
table.json("meta").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
})
return knex.schema.createTable('auth', (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('type', 30).notNull();
table.string('secret').notNull();
table.json('meta').notNull();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("is_disabled").notNull().unsigned().defaultTo(0);
table.string("email").notNull();
table.string("name").notNull();
table.string("nickname").notNull();
table.string("avatar").notNull();
table.json("roles").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer('is_disabled').notNull().unsigned().defaultTo(0);
table.string('email').notNull();
table.string('name').notNull();
table.string('nickname').notNull();
table.string('avatar').notNull();
table.json('roles').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("visibility").notNull();
table.string("proxy_hosts").notNull();
table.string("redirection_hosts").notNull();
table.string("dead_hosts").notNull();
table.string("streams").notNull();
table.string("access_lists").notNull();
table.string("certificates").notNull();
table.unique("user_id");
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('visibility').notNull();
table.string('proxy_hosts').notNull();
table.string('redirection_hosts').notNull();
table.string('dead_hosts').notNull();
table.string('streams').notNull();
table.string('access_lists').notNull();
table.string('certificates').notNull();
table.unique('user_id');
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_ip").notNull();
table.integer("forward_port").notNull().unsigned();
table.integer("access_list_id").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("caching_enabled").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.string('forward_ip').notNull();
table.integer('forward_port').notNull().unsigned();
table.string('root_dir').notNull();
table.string('index_file').notNull();
table.integer('static').notNull().unsigned().defaultTo(0);
table.integer('access_list_id').notNull().unsigned().defaultTo(0);
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer('caching_enabled').notNull().unsigned().defaultTo(0);
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.string("forward_domain_name").notNull();
table.integer("preserve_path").notNull().unsigned().defaultTo(0);
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.integer("block_exploits").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.string('forward_domain_name').notNull();
table.integer('preserve_path').notNull().unsigned().defaultTo(0);
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.integer('block_exploits').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.json("domain_names").notNull();
table.integer("certificate_id").notNull().unsigned().defaultTo(0);
table.integer("ssl_forced").notNull().unsigned().defaultTo(0);
table.text("advanced_config").notNull().defaultTo("");
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.json('domain_names').notNull();
table.integer('certificate_id').notNull().unsigned().defaultTo(0);
table.integer('ssl_forced').notNull().unsigned().defaultTo(0);
table.text('advanced_config').notNull().defaultTo('');
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.integer("incoming_port").notNull().unsigned();
table.string("forward_ip").notNull();
table.integer("forwarding_port").notNull().unsigned();
table.integer("tcp_forwarding").notNull().unsigned().defaultTo(0);
table.integer("udp_forwarding").notNull().unsigned().defaultTo(0);
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.integer('incoming_port').notNull().unsigned();
table.string('forward_ip').notNull();
table.integer('forwarding_port').notNull().unsigned();
table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0);
table.integer('udp_forwarding').notNull().unsigned().defaultTo(0);
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("name").notNull();
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string('name').notNull();
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("owner_user_id").notNull().unsigned();
table.integer("is_deleted").notNull().unsigned().defaultTo(0);
table.string("provider").notNull();
table.string("nice_name").notNull().defaultTo("");
table.json("domain_names").notNull();
table.dateTime("expires_on").notNull();
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('owner_user_id').notNull().unsigned();
table.integer('is_deleted').notNull().unsigned().defaultTo(0);
table.string('provider').notNull();
table.string('nice_name').notNull().defaultTo('');
table.json('domain_names').notNull();
table.dateTime('expires_on').notNull();
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("username").notNull();
table.string("password").notNull();
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('username').notNull();
table.string('password').notNull();
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${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.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("user_id").notNull().unsigned();
table.string("object_type").notNull().defaultTo("");
table.integer("object_id").notNull().unsigned().defaultTo(0);
table.string("action").notNull();
table.json("meta").notNull();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
table.string('object_type').notNull().defaultTo('');
table.integer('object_id').notNull().unsigned().defaultTo(0);
table.string('action').notNull();
table.json('meta').notNull();
});
})
.then(() => {
logger.info(`[${migrateName}] audit_log Table created`);
logger.info('[' + migrate_name + '] audit_log Table created');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "websockets";
const migrate_name = 'websockets';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,29 +7,29 @@ const migrateName = "websockets";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.integer("allow_websocket_upgrade").notNull().unsigned().defaultTo(0);
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('allow_websocket_upgrade').notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
logger.info('[' + migrate_name + '] proxy_host Table altered');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };
};

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "forward_host";
const migrate_name = 'forward_host';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,17 @@ const migrateName = "forward_host";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.renameColumn("forward_ip", "forward_host");
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.renameColumn('forward_ip', 'forward_host');
})
.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
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };
};

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "http2_support";
const migrate_name = 'http2_support';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,31 +7,31 @@ const migrateName = "http2_support";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.integer("http2_support").notNull().unsigned().defaultTo(0);
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('http2_support').notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("http2_support").notNull().unsigned().defaultTo(0);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('http2_support').notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("http2_support").notNull().unsigned().defaultTo(0);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('http2_support').notNull().unsigned().defaultTo(0);
});
})
.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
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "forward_scheme";
const migrate_name = 'forward_scheme';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,17 @@ const migrateName = "forward_scheme";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.string("forward_scheme").notNull().defaultTo("http");
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.string('forward_scheme').notNull().defaultTo('http');
})
.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
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "disabled";
const migrate_name = 'disabled';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,38 +7,38 @@ const migrateName = "disabled";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.integer("enabled").notNull().unsigned().defaultTo(1);
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('enabled').notNull().unsigned().defaultTo(1);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("enabled").notNull().unsigned().defaultTo(1);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('enabled').notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("enabled").notNull().unsigned().defaultTo(1);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('enabled').notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table altered`);
logger.info('[' + migrate_name + '] dead_host Table altered');
return knex.schema.table("stream", (stream) => {
stream.integer("enabled").notNull().unsigned().defaultTo(1);
return knex.schema.table('stream', function (stream) {
stream.integer('enabled').notNull().unsigned().defaultTo(1);
});
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
logger.info('[' + migrate_name + '] stream Table altered');
});
};
@@ -47,11 +46,10 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "custom_locations";
const migrate_name = 'custom_locations';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -9,17 +8,17 @@ const migrateName = "custom_locations";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.json("locations");
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.json('locations');
})
.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
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "hsts";
const migrate_name = 'hsts';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,34 +7,34 @@ const migrateName = "hsts";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("proxy_host", (proxy_host) => {
proxy_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
proxy_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
})
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
proxy_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
})
.then(() => {
logger.info(`[${migrateName}] proxy_host Table altered`);
logger.info('[' + migrate_name + '] proxy_host Table altered');
return knex.schema.table("redirection_host", (redirection_host) => {
redirection_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
redirection_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
return knex.schema.table('redirection_host', function (redirection_host) {
redirection_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
redirection_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
logger.info('[' + migrate_name + '] redirection_host Table altered');
return knex.schema.table("dead_host", (dead_host) => {
dead_host.integer("hsts_enabled").notNull().unsigned().defaultTo(0);
dead_host.integer("hsts_subdomains").notNull().unsigned().defaultTo(0);
return knex.schema.table('dead_host', function (dead_host) {
dead_host.integer('hsts_enabled').notNull().unsigned().defaultTo(0);
dead_host.integer('hsts_subdomains').notNull().unsigned().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] dead_host Table altered`);
logger.info('[' + migrate_name + '] dead_host Table altered');
});
};
@@ -43,11 +42,10 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "settings";
const migrate_name = 'settings';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,10 +7,11 @@ const migrateName = "settings";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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.createTable('setting', (table) => {
table.string('id').notNull().primary();
@@ -21,7 +21,7 @@ const up = (knex) => {
table.json('meta').notNull();
})
.then(() => {
logger.info(`[${migrateName}] setting Table created`);
logger.info('[' + migrate_name + '] setting Table created');
});
};
@@ -29,11 +29,10 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down the initial data.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "access_list_client";
const migrate_name = 'access_list_client';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,30 +7,32 @@ const migrateName = "access_list_client";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
exports.up = function (knex/*, Promise*/) {
return knex.schema
.createTable("access_list_client", (table) => {
table.increments().primary();
table.dateTime("created_on").notNull();
table.dateTime("modified_on").notNull();
table.integer("access_list_id").notNull().unsigned();
table.string("address").notNull();
table.string("directive").notNull();
table.json("meta").notNull();
})
.then(() => {
logger.info(`[${migrateName}] access_list_client Table created`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table("access_list", (access_list) => {
access_list.integer("satify_any").notNull().defaultTo(0);
return knex.schema.createTable('access_list_client', (table) => {
table.increments().primary();
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('access_list_id').notNull().unsigned();
table.string('address').notNull();
table.string('directive').notNull();
table.json('meta').notNull();
})
.then(function () {
logger.info('[' + migrate_name + '] access_list_client Table created');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('satify_any').notNull().defaultTo(0);
});
})
.then(() => {
logger.info(`[${migrateName}] access_list Table altered`);
logger.info('[' + migrate_name + '] access_list Table altered');
});
};
@@ -39,14 +40,14 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @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.dropTable("access_list_client").then(() => {
logger.info(`[${migrateName}] access_list_client Table dropped`);
});
return knex.schema.dropTable('access_list_client')
.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 migrateName = "access_list_client_fix";
const migrate_name = 'access_list_client_fix';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,17 @@ const migrateName = "access_list_client_fix";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @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
.table("access_list", (access_list) => {
access_list.renameColumn("satify_any", "satisfy_any");
})
return knex.schema.table('access_list', function (access_list) {
access_list.renameColumn('satify_any', 'satisfy_any');
})
.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
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const down = (_knex) => {
logger.warn(`[${migrateName}] You can't migrate down this one.`);
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "pass_auth";
const migrate_name = 'pass_auth';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,18 @@ const migrateName = "pass_auth";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
exports.up = function (knex/*, Promise*/) {
return knex.schema
.table("access_list", (access_list) => {
access_list.integer("pass_auth").notNull().defaultTo(1);
})
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('access_list', function (access_list) {
access_list.integer('pass_auth').notNull().defaultTo(1);
})
.then(() => {
logger.info(`[${migrateName}] access_list Table altered`);
logger.info('[' + migrate_name + '] access_list Table altered');
});
};
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @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
.table("access_list", (access_list) => {
access_list.dropColumn("pass_auth");
})
return knex.schema.table('access_list', function (access_list) {
access_list.dropColumn('pass_auth');
})
.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 migrateName = "redirection_scheme";
const migrate_name = 'redirection_scheme';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,18 @@ const migrateName = "redirection_scheme";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
exports.up = function (knex/*, Promise*/) {
return knex.schema
.table("redirection_host", (table) => {
table.string("forward_scheme").notNull().defaultTo("$scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('redirection_host', (table) => {
table.string('forward_scheme').notNull().defaultTo('$scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @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
.table("redirection_host", (table) => {
table.dropColumn("forward_scheme");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table('redirection_host', (table) => {
table.dropColumn('forward_scheme');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};
export { up, down };

View File

@@ -1,6 +1,5 @@
import { migrate as logger } from "../logger.js";
const migrateName = "redirection_status_code";
const migrate_name = 'redirection_status_code';
const logger = require('../logger').migrate;
/**
* Migrate
@@ -8,17 +7,18 @@ const migrateName = "redirection_status_code";
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
const up = (knex) => {
logger.info(`[${migrateName}] Migrating Up...`);
exports.up = function (knex/*, Promise*/) {
return knex.schema
.table("redirection_host", (table) => {
table.integer("forward_http_code").notNull().unsigned().defaultTo(302);
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('redirection_host', (table) => {
table.integer('forward_http_code').notNull().unsigned().defaultTo(302);
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};
@@ -26,18 +26,16 @@ const up = (knex) => {
* Undo Migrate
*
* @param {Object} knex
* @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
.table("redirection_host", (table) => {
table.dropColumn("forward_http_code");
})
.then(() => {
logger.info(`[${migrateName}] redirection_host Table altered`);
return knex.schema.table('redirection_host', (table) => {
table.dropColumn('forward_http_code');
})
.then(function () {
logger.info('[' + migrate_name + '] redirection_host Table altered');
});
};
export { up, down };

View File

@@ -1,43 +0,0 @@
import { migrate as logger } from "../logger.js";
const migrateName = "stream_domain";
/**
* 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.renameColumn("forward_ip", "forwarding_host");
})
.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.renameColumn("forwarding_host", "forward_ip");
})
.then(() => {
logger.info(`[${migrateName}] stream Table altered`);
});
};
export { up, down };

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:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessListAuth from "./access_list_auth.js";
import AccessListClient from "./access_list_client.js";
import now from "./now_helper.js";
import ProxyHostModel from "./proxy_host.js";
import User from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const AccessListAuth = require('./access_list_auth');
const AccessListClient = require('./access_list_client');
const now = require('./now_helper');
Model.knex(db);
const boolFields = ["is_deleted", "satisfy_any", "pass_auth"];
class AccessList extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'AccessList';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'access_list';
}
static get name() {
return "AccessList";
static get jsonAttributes () {
return ['meta'];
}
static get tableName() {
return "access_list";
}
static get relationMappings () {
const ProxyHost = require('./proxy_host');
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "access_list.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
join: {
from: 'access_list.owner_user_id',
to: 'user.id'
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
},
items: {
relation: Model.HasManyRelation,
relation: Model.HasManyRelation,
modelClass: AccessListAuth,
join: {
from: "access_list.id",
to: "access_list_auth.access_list_id",
join: {
from: 'access_list.id',
to: 'access_list_auth.access_list_id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
}
},
clients: {
relation: Model.HasManyRelation,
relation: Model.HasManyRelation,
modelClass: AccessListClient,
join: {
from: "access_list.id",
to: "access_list_client.access_list_id",
join: {
from: 'access_list.id',
to: 'access_list_client.access_list_id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'access_list_id', 'meta']);
}
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: ProxyHostModel,
join: {
from: "access_list.id",
to: "proxy_host.access_list_id",
relation: Model.HasManyRelation,
modelClass: ProxyHost,
join: {
from: 'access_list.id',
to: 'proxy_host.access_list_id'
},
modify: (qb) => {
qb.where("proxy_host.is_deleted", 0);
},
},
modify: function (qb) {
qb.where('proxy_host.is_deleted', 0);
qb.omit(['is_deleted', 'meta']);
}
}
};
}
get satisfy() {
return this.satisfy_any ? 'satisfy any' : 'satisfy all';
}
get passauth() {
return this.pass_auth ? '' : 'proxy_set_header Authorization "";';
}
}
export default AccessList;
module.exports = AccessList;

View File

@@ -1,55 +1,55 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db);
class AccessListAuth extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
}
static get name() {
return "AccessListAuth";
static get name () {
return 'AccessListAuth';
}
static get tableName() {
return "access_list_auth";
static get tableName () {
return 'access_list_auth';
}
static get jsonAttributes() {
return ["meta"];
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings() {
static get relationMappings () {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_auth.access_list_id",
to: "access_list.id",
relation: Model.HasOneRelation,
modelClass: require('./access_list'),
join: {
from: 'access_list_auth.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(['created_on', 'modified_on', 'is_deleted', 'access_list_id']);
}
}
};
}
}
export default AccessListAuth;
module.exports = AccessListAuth;

View File

@@ -1,55 +1,59 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import accessListModel from "./access_list.js";
import now from "./now_helper.js";
const db = require('../db');
const Model = require('objection').Model;
const now = require('./now_helper');
Model.knex(db);
class AccessListClient extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
}
static get name() {
return "AccessListClient";
static get name () {
return 'AccessListClient';
}
static get tableName() {
return "access_list_client";
static get tableName () {
return 'access_list_client';
}
static get jsonAttributes() {
return ["meta"];
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings() {
static get relationMappings () {
return {
access_list: {
relation: Model.HasOneRelation,
modelClass: accessListModel,
join: {
from: "access_list_client.access_list_id",
to: "access_list.id",
relation: Model.HasOneRelation,
modelClass: require('./access_list'),
join: {
from: 'access_list_client.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(['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:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import now from "./now_helper.js";
import User from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
class AuditLog extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
}
static get name() {
return "AuditLog";
static get name () {
return 'AuditLog';
}
static get tableName() {
return "audit_log";
static get tableName () {
return 'audit_log';
}
static get jsonAttributes() {
return ["meta"];
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings() {
static get relationMappings () {
return {
user: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "audit_log.user_id",
to: "user.id",
join: {
from: 'audit_log.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:
// http://vincit.github.io/objection.js/
import bcrypt from "bcrypt";
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import now from "./now_helper.js";
import User from "./user.js";
const bcrypt = require('bcrypt');
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
const boolFields = ["is_deleted"];
function encryptPassword () {
/* jshint -W040 */
let _this = this;
function encryptPassword() {
if (this.type === "password" && this.secret) {
return bcrypt.hash(this.secret, 13).then((hash) => {
this.secret = hash;
});
if (_this.type === 'password' && _this.secret) {
return bcrypt.hash(_this.secret, 13)
.then(function (hash) {
_this.secret = hash;
});
}
return null;
}
class Auth extends Model {
$beforeInsert(queryContext) {
this.created_on = now();
$beforeInsert (queryContext) {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
return encryptPassword.apply(this, queryContext);
}
$beforeUpdate(queryContext) {
$beforeUpdate (queryContext) {
this.modified_on = now();
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
*
* @param {String} password
* @returns {Promise}
*/
verifyPassword(password) {
verifyPassword (password) {
return bcrypt.compare(password, this.secret);
}
static get name() {
return "Auth";
static get name () {
return 'Auth';
}
static get tableName() {
return "auth";
static get tableName () {
return 'auth';
}
static get jsonAttributes() {
return ["meta"];
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings() {
static get relationMappings () {
return {
user: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "auth.user_id",
to: "user.id",
join: {
from: 'auth.user_id',
to: 'user.id'
},
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:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import deadHostModel from "./dead_host.js";
import now from "./now_helper.js";
import proxyHostModel from "./proxy_host.js";
import redirectionHostModel from "./redirection_host.js";
import userModel from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
const boolFields = ["is_deleted"];
class Certificate extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for expires_on
if (typeof this.expires_on === "undefined") {
if (typeof this.expires_on === 'undefined') {
this.expires_on = now();
}
// Default for domain_names
if (typeof this.domain_names === "undefined") {
if (typeof this.domain_names === 'undefined') {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'Certificate';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'certificate';
}
static get name() {
return "Certificate";
static get jsonAttributes () {
return ['domain_names', 'meta'];
}
static get tableName() {
return "certificate";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
static get relationMappings () {
return {
owner: {
relation: Model.HasOneRelation,
modelClass: userModel,
join: {
from: "certificate.owner_user_id",
to: "user.id",
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'certificate.owner_user_id',
to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
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);
},
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
}
};
}
}
export default Certificate;
module.exports = Certificate;

View File

@@ -1,92 +1,81 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
const boolFields = ["is_deleted", "ssl_forced", "http2_support", "enabled", "hsts_enabled", "hsts_subdomains"];
class DeadHost extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
if (typeof this.domain_names === 'undefined') {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'DeadHost';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'dead_host';
}
static get name() {
return "DeadHost";
static get jsonAttributes () {
return ['domain_names', 'meta'];
}
static get tableName() {
return "dead_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
static get relationMappings () {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "dead_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
join: {
from: 'dead_host.owner_user_id',
to: 'user.id'
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "dead_host.certificate_id",
to: "certificate.id",
join: {
from: 'dead_host.certificate_id',
to: 'certificate.id'
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
modify: function (qb) {
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";
import db from "../db.js";
import { isSqlite } from "../lib/config.js";
const db = require('../db');
const config = require('config');
const Model = require('objection').Model;
Model.knex(db);
export default () => {
if (isSqlite()) {
return Model.raw("datetime('now','localtime')");
module.exports = function () {
if (config.database.knex && config.database.knex.client === 'sqlite3') {
return Model.raw('datetime(\'now\',\'localtime\')');
} else {
return Model.raw('NOW()');
}
return Model.raw("NOW()");
};

View File

@@ -1,114 +1,94 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import AccessList from "./access_list.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const AccessList = require('./access_list');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
const boolFields = [
"is_deleted",
"ssl_forced",
"caching_enabled",
"block_exploits",
"allow_websocket_upgrade",
"http2_support",
"enabled",
"hsts_enabled",
"hsts_subdomains",
];
class ProxyHost extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
if (typeof this.domain_names === 'undefined') {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'ProxyHost';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'proxy_host';
}
static get name() {
return "ProxyHost";
static get jsonAttributes () {
return ['domain_names', 'meta', 'locations'];
}
static get tableName() {
return "proxy_host";
}
static get jsonAttributes() {
return ["domain_names", "meta", "locations"];
}
static get relationMappings() {
static get relationMappings () {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "proxy_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
join: {
from: 'proxy_host.owner_user_id',
to: 'user.id'
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
},
access_list: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: AccessList,
join: {
from: "proxy_host.access_list_id",
to: "access_list.id",
},
modify: (qb) => {
qb.where("access_list.is_deleted", 0);
join: {
from: 'proxy_host.access_list_id',
to: 'access_list.id'
},
modify: function (qb) {
qb.where('access_list.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted']);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "proxy_host.certificate_id",
to: "certificate.id",
join: {
from: 'proxy_host.certificate_id',
to: 'certificate.id'
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
modify: function (qb) {
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:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
const Certificate = require('./certificate');
const now = require('./now_helper');
Model.knex(db);
const boolFields = [
"is_deleted",
"enabled",
"preserve_path",
"ssl_forced",
"block_exploits",
"hsts_enabled",
"hsts_subdomains",
"http2_support",
];
class RedirectionHost extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for domain_names
if (typeof this.domain_names === "undefined") {
if (typeof this.domain_names === 'undefined') {
this.domain_names = [];
}
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
this.domain_names.sort();
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
// Sort domain_names
if (typeof this.domain_names !== "undefined") {
if (typeof this.domain_names !== 'undefined') {
this.domain_names.sort();
}
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'RedirectionHost';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'redirection_host';
}
static get name() {
return "RedirectionHost";
static get jsonAttributes () {
return ['domain_names', 'meta'];
}
static get tableName() {
return "redirection_host";
}
static get jsonAttributes() {
return ["domain_names", "meta"];
}
static get relationMappings() {
static get relationMappings () {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "redirection_host.owner_user_id",
to: "user.id",
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
join: {
from: 'redirection_host.owner_user_id',
to: 'user.id'
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
},
certificate: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "redirection_host.certificate_id",
to: "certificate.id",
join: {
from: 'redirection_host.certificate_id',
to: 'certificate.id'
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
modify: function (qb) {
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:
// http://vincit.github.io/objection.js/
import { Model } from "objection";
import db from "../db.js";
const db = require('../db');
const Model = require('objection').Model;
Model.knex(db);
@@ -27,4 +27,4 @@ class Setting extends Model {
}
}
export default Setting;
module.exports = Setting;

View File

@@ -1,77 +1,56 @@
import { Model } from "objection";
import db from "../db.js";
import { convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js";
import Certificate from "./certificate.js";
import now from "./now_helper.js";
import User from "./user.js";
// 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);
const boolFields = ["is_deleted", "enabled", "tcp_forwarding", "udp_forwarding"];
class Stream extends Model {
$beforeInsert() {
this.created_on = now();
$beforeInsert () {
this.created_on = now();
this.modified_on = now();
// Default for meta
if (typeof this.meta === "undefined") {
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
$beforeUpdate() {
$beforeUpdate () {
this.modified_on = now();
}
$parseDatabaseJson(json) {
const thisJson = super.$parseDatabaseJson(json);
return convertIntFieldsToBool(thisJson, boolFields);
static get name () {
return 'Stream';
}
$formatDatabaseJson(json) {
const thisJson = convertBoolFieldsToInt(json, boolFields);
return super.$formatDatabaseJson(thisJson);
static get tableName () {
return 'stream';
}
static get name() {
return "Stream";
static get jsonAttributes () {
return ['meta'];
}
static get tableName() {
return "stream";
}
static get jsonAttributes() {
return ["meta"];
}
static get relationMappings() {
static get relationMappings () {
return {
owner: {
relation: Model.HasOneRelation,
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: "stream.owner_user_id",
to: "user.id",
join: {
from: 'stream.owner_user_id',
to: 'user.id'
},
modify: (qb) => {
qb.where("user.is_deleted", 0);
},
},
certificate: {
relation: Model.HasOneRelation,
modelClass: Certificate,
join: {
from: "stream.certificate_id",
to: "certificate.id",
},
modify: (qb) => {
qb.where("certificate.is_deleted", 0);
},
},
modify: function (qb) {
qb.where('user.is_deleted', 0);
qb.omit(['id', 'created_on', 'modified_on', 'is_deleted', 'email', 'roles']);
}
}
};
}
}
export default Stream;
module.exports = Stream;

View File

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

View File

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

View File

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

View File

@@ -1,49 +1,47 @@
{
"name": "nginx-proxy-manager",
"version": "2.0.0",
"version": "0.0.0",
"description": "A beautiful interface for creating Nginx endpoints",
"author": "Jamie Curnow <jc@jc21.com>",
"license": "MIT",
"main": "index.js",
"type": "module",
"scripts": {
"lint": "biome lint",
"prettier": "biome format --write .",
"validate-schema": "node validate-schema.js"
},
"main": "js/index.js",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.0",
"ajv": "^8.17.1",
"archiver": "^5.3.0",
"ajv": "^6.12.0",
"batchflow": "^0.4.0",
"bcrypt": "^5.0.0",
"body-parser": "^1.20.3",
"body-parser": "^1.19.0",
"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",
"gravatar": "^1.8.0",
"jsonwebtoken": "^9.0.0",
"knex": "2.4.2",
"liquidjs": "10.6.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"mysql2": "^3.11.1",
"html-entities": "^1.2.1",
"json-schema-ref-parser": "^8.0.0",
"jsonwebtoken": "^8.5.1",
"knex": "^0.20.13",
"liquidjs": "^9.11.10",
"lodash": "^4.17.19",
"moment": "^2.24.0",
"mysql": "^2.18.1",
"node-rsa": "^1.0.8",
"objection": "3.0.1",
"nodemon": "^2.0.2",
"objection": "^2.1.3",
"path": "^0.12.7",
"pg": "^8.13.1",
"signale": "1.4.0",
"sqlite3": "5.1.6",
"temp-write": "^4.0.0"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@biomejs/biome": "^2.2.4",
"chalk": "4.1.2",
"nodemon": "^2.0.2"
"pg": "^7.12.1",
"restler": "^3.4.0",
"signale": "^1.4.0",
"sqlite3": "^4.1.1",
"temp-write": "^4.0.0",
"unix-timestamp": "^0.2.0"
},
"signale": {
"displayDate": true,
"displayTimestamp": true
},
"author": "Jamie Curnow <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,51 @@
const express = require('express');
const pjson = require('../../package.json');
const error = require('../../lib/error');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* Health Check
* GET /api
*/
router.get('/', (req, res/*, next*/) => {
let version = pjson.version.split('-').shift().split('.');
res.status(200).send({
status: 'OK',
version: {
major: parseInt(version.shift(), 10),
minor: parseInt(version.shift(), 10),
revision: parseInt(version.shift(), 10)
}
});
});
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));
router.use('/nginx/streams', require('./nginx/streams'));
router.use('/nginx/access-lists', require('./nginx/access_lists'));
router.use('/nginx/certificates', require('./nginx/certificates'));
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, function (req, res, next) {
req.params.page = req.params['0'];
next(new error.ItemNotFoundError(req.params.page));
});
module.exports = router;

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

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

View File

@@ -0,0 +1,96 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalSetting = require('../../internal/setting');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/settings
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/settings
*
* Retrieve all settings
*/
.get((req, res, next) => {
internalSetting.getAll(res.locals.access)
.then((rows) => {
res.status(200)
.send(rows);
})
.catch(next);
});
/**
* Specific setting
*
* /api/settings/something
*/
router
.route('/:setting_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /settings/something
*
* Retrieve a specific setting
*/
.get((req, res, next) => {
validator({
required: ['setting_id'],
additionalProperties: false,
properties: {
setting_id: {
$ref: 'definitions#/definitions/setting_id'
}
}
}, {
setting_id: req.params.setting_id
})
.then((data) => {
return internalSetting.get(res.locals.access, {
id: data.setting_id
});
})
.then((row) => {
res.status(200)
.send(row);
})
.catch(next);
})
/**
* PUT /api/settings/something
*
* Update and existing setting
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/settings#/links/1/schema'}, req.body)
.then((payload) => {
payload.id = req.params.setting_id;
return internalSetting.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -0,0 +1,54 @@
const express = require('express');
const jwtdecode = require('../../lib/express/jwt-decode');
const internalToken = require('../../internal/token');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
/**
* GET /tokens
*
* Get a new Token, given they already have a token they want to refresh
* We also piggy back on to this method, allowing admins to get tokens
* for services like Job board and Worker.
*/
.get(jwtdecode(), (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
})
/**
* POST /tokens
*
* Create a new Token
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/tokens#/links/0/schema'}, req.body)
.then((payload) => {
return internalToken.getTokenFromEmail(payload);
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
module.exports = router;

239
backend/routes/api/users.js Normal file
View File

@@ -0,0 +1,239 @@
const express = require('express');
const validator = require('../../lib/validator');
const jwtdecode = require('../../lib/express/jwt-decode');
const userIdFromMe = require('../../lib/express/user-id-from-me');
const internalUser = require('../../internal/user');
const apiValidator = require('../../lib/validator/api');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
/**
* /api/users
*/
router
.route('/')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/users
*
* Retrieve all users
*/
.get((req, res, next) => {
validator({
additionalProperties: false,
properties: {
expand: {
$ref: 'definitions#/definitions/expand'
},
query: {
$ref: 'definitions#/definitions/query'
}
}
}, {
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null),
query: (typeof req.query.query === 'string' ? req.query.query : null)
})
.then((data) => {
return internalUser.getAll(res.locals.access, data.expand, data.query);
})
.then((users) => {
res.status(200)
.send(users);
})
.catch(next);
})
/**
* POST /api/users
*
* Create a new User
*/
.post((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/1/schema'}, req.body)
.then((payload) => {
return internalUser.create(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user
*
* /api/users/123
*/
router
.route('/:user_id')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* GET /users/123 or /users/me
*
* Retrieve a specific user
*/
.get((req, res, next) => {
validator({
required: ['user_id'],
additionalProperties: false,
properties: {
user_id: {
$ref: 'definitions#/definitions/id'
},
expand: {
$ref: 'definitions#/definitions/expand'
}
}
}, {
user_id: req.params.user_id,
expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null)
})
.then((data) => {
return internalUser.get(res.locals.access, {
id: data.user_id,
expand: data.expand,
omit: internalUser.getUserOmisionsByAccess(res.locals.access, data.user_id)
});
})
.then((user) => {
res.status(200)
.send(user);
})
.catch(next);
})
/**
* PUT /api/users/123
*
* Update and existing user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/2/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.update(res.locals.access, payload);
})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
})
/**
* DELETE /api/users/123
*
* Update and existing user
*/
.delete((req, res, next) => {
internalUser.delete(res.locals.access, {id: req.params.user_id})
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});
/**
* Specific user auth
*
* /api/users/123/auth
*/
router
.route('/:user_id/auth')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/auth
*
* Update password for a user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/4/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPassword(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user permissions
*
* /api/users/123/permissions
*/
router
.route('/:user_id/permissions')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* PUT /api/users/123/permissions
*
* Set some or all permissions for a user
*/
.put((req, res, next) => {
apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body)
.then((payload) => {
payload.id = req.params.user_id;
return internalUser.setPermissions(res.locals.access, payload);
})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
/**
* Specific user login as
*
* /api/users/123/login
*/
router
.route('/:user_id/login')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* POST /api/users/123/login
*
* Log in as a user
*/
.post((req, res, next) => {
internalUser.loginAs(res.locals.access, {id: parseInt(req.params.user_id, 10)})
.then((result) => {
res.status(201)
.send(result);
})
.catch(next);
});
module.exports = router;

View File

@@ -1,55 +0,0 @@
import express from "express";
import internalAuditLog from "../internal/audit-log.js";
import jwtdecode from "../lib/express/jwt-decode.js";
import validator from "../lib/validator/index.js";
import { express as logger } from "../logger.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/audit-log
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/audit-log
*
* Retrieve all logs
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAuditLog.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

View File

@@ -1,66 +0,0 @@
import express from "express";
import errs from "../lib/error.js";
import pjson from "../package.json" with { type: "json" };
import { isSetup } from "../setup.js";
import auditLogRoutes from "./audit-log.js";
import accessListsRoutes from "./nginx/access_lists.js";
import certificatesHostsRoutes from "./nginx/certificates.js";
import deadHostsRoutes from "./nginx/dead_hosts.js";
import proxyHostsRoutes from "./nginx/proxy_hosts.js";
import redirectionHostsRoutes from "./nginx/redirection_hosts.js";
import streamsRoutes from "./nginx/streams.js";
import reportsRoutes from "./reports.js";
import schemaRoutes from "./schema.js";
import settingsRoutes from "./settings.js";
import tokensRoutes from "./tokens.js";
import usersRoutes from "./users.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* Health Check
* GET /api
*/
router.get("/", async (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
res.status(200).send({
status: "OK",
setup,
version: {
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
});
router.use("/schema", schemaRoutes);
router.use("/tokens", tokensRoutes);
router.use("/users", usersRoutes);
router.use("/audit-log", auditLogRoutes);
router.use("/reports", reportsRoutes);
router.use("/settings", settingsRoutes);
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
router.use("/nginx/dead-hosts", deadHostsRoutes);
router.use("/nginx/streams", streamsRoutes);
router.use("/nginx/access-lists", accessListsRoutes);
router.use("/nginx/certificates", certificatesHostsRoutes);
/**
* API 404 for all other routes
*
* ALL /api/*
*/
router.all(/(.+)/, (req, _, next) => {
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
});
export default router;

View File

@@ -1,155 +0,0 @@
import express from "express";
import internalAccessList from "../../internal/access-list.js";
import jwtdecode from "../../lib/express/jwt-decode.js";
import apiValidator from "../../lib/validator/api.js";
import validator from "../../lib/validator/index.js";
import { express as logger } from "../../logger.js";
import { getValidationSchema } from "../../schema/index.js";
const router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true,
});
/**
* /api/nginx/access-lists
*/
router
.route("/")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists
*
* Retrieve all access-lists
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
additionalProperties: false,
properties: {
expand: {
$ref: "common#/properties/expand",
},
query: {
$ref: "common#/properties/query",
},
},
},
{
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
query: typeof req.query.query === "string" ? req.query.query : null,
},
);
const rows = await internalAccessList.getAll(res.locals.access, data.expand, data.query);
res.status(200).send(rows);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* POST /api/nginx/access-lists
*
* Create a new access-list
*/
.post(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists", "post"), req.body);
const result = await internalAccessList.create(res.locals.access, payload);
res.status(201).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* Specific access-list
*
* /api/nginx/access-lists/123
*/
router
.route("/:list_id")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
/**
* GET /api/nginx/access-lists/123
*
* Retrieve a specific access-list
*/
.get(async (req, res, next) => {
try {
const data = await validator(
{
required: ["list_id"],
additionalProperties: false,
properties: {
list_id: {
$ref: "common#/properties/id",
},
expand: {
$ref: "common#/properties/expand",
},
},
},
{
list_id: req.params.list_id,
expand: typeof req.query.expand === "string" ? req.query.expand.split(",") : null,
},
);
const row = await internalAccessList.get(res.locals.access, {
id: Number.parseInt(data.list_id, 10),
expand: data.expand,
});
res.status(200).send(row);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* PUT /api/nginx/access-lists/123
*
* Update and existing access-list
*/
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/nginx/access-lists/{listID}", "put"), req.body);
payload.id = Number.parseInt(req.params.list_id, 10);
const result = await internalAccessList.update(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* DELETE /api/nginx/access-lists/123
*
* Delete and existing access-list
*/
.delete(async (req, res, next) => {
try {
const result = await internalAccessList.delete(res.locals.access, {
id: Number.parseInt(req.params.list_id, 10),
});
res.status(200).send(result);
} catch (err) {
logger.debug(`${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
export default router;

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