Version 3 starter

This commit is contained in:
Jamie Curnow
2021-06-14 19:29:35 +10:00
parent 60fc57431a
commit 6205434140
642 changed files with 25817 additions and 32319 deletions

19
.gitignore vendored
View File

@@ -1,5 +1,20 @@
.DS_Store
.idea .idea
.env
.DS_Store
._* ._*
*.code-workspace
vendor
dist
backend/config.json
backend/internal/api/handler/assets
test/node_modules
*/node_modules
docs/.vuepress/dist
frontend/build
frontend/yarn-error.log
frontend/yarn.lock
frontend/.npmrc
test/cypress/fixtures/example.json
.vscode .vscode
certbot-help.txt docker-build
data

View File

@@ -1 +1 @@
2.9.3 3.0.0a

27
DEV-README.md Normal file
View File

@@ -0,0 +1,27 @@
# Nginx Proxy Manager 3
WIP
## Usage
environment variables
## Building
### Backend API Server
```bash
go build -ldflags="-X main.commit=$(git log -n 1 --format=%h)" -o bin/server ./cmd/server/main.go
```
## Development
```bash
git clone nginxproxymanager
cd nginxproxymanager
./scripts/start-dev
curl http://127.0.0.1:3000/api/
```

168
Jenkinsfile vendored
View File

@@ -8,14 +8,17 @@ pipeline {
ansiColor('xterm') ansiColor('xterm')
} }
environment { environment {
IMAGE = "nginx-proxy-manager" IMAGE = 'nginx-proxy-manager'
BUILD_VERSION = getVersion() BUILD_VERSION = getVersion()
MAJOR_VERSION = "2" BUILD_COMMIT = getCommit()
MAJOR_VERSION = '3'
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}" BRANCH_LOWER = "${BRANCH_NAME.toLowerCase().replaceAll('/', '-')}"
COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}" COMPOSE_PROJECT_NAME = "npm_${BRANCH_LOWER}_${BUILD_NUMBER}"
COMPOSE_FILE = 'docker/docker-compose.ci.yml' COMPOSE_FILE = 'docker/docker-compose.ci.yml'
COMPOSE_INTERACTIVE_NO_CLI = 1 COMPOSE_INTERACTIVE_NO_CLI = 1
BUILDX_NAME = "${COMPOSE_PROJECT_NAME}" BUILDX_NAME = "${COMPOSE_PROJECT_NAME}"
DOCS_BUCKET = 'jc21-npm-site-next' // TODO: change to prod when official
DOCS_CDN = 'E2Z0128EHS0Q23' // TODO: same
} }
stages { stages {
stage('Environment') { stage('Environment') {
@@ -45,10 +48,9 @@ pipeline {
} }
stage('Versions') { stage('Versions') {
steps { steps {
// Is this frontend version stuff still applicable?
sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json' sh 'cat frontend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge frontend/package.json'
sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"' sh 'echo -e "\\E[1;36mFrontend Version is:\\E[1;33m $(cat frontend/package.json | jq -r .version)\\E[0m"'
sh 'cat backend/package.json | jq --arg BUILD_VERSION "${BUILD_VERSION}" \'.version = $BUILD_VERSION\' | sponge backend/package.json'
sh 'echo -e "\\E[1;36mBackend Version is:\\E[1;33m $(cat backend/package.json | jq -r .version)\\E[0m"'
sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md' sh 'sed -i -E "s/(version-)[0-9]+\\.[0-9]+\\.[0-9]+(-green)/\\1${BUILD_VERSION}\\2/" README.md'
} }
} }
@@ -56,78 +58,57 @@ pipeline {
} }
stage('Frontend') { stage('Frontend') {
steps { steps {
sh './scripts/frontend-build' sh './scripts/ci/frontend-build'
}
post {
always {
junit 'frontend/eslint.xml'
junit 'frontend/junit.xml'
}
} }
} }
stage('Backend') { stage('Backend') {
steps { steps {
echo 'Checking Syntax ...' withCredentials([usernamePassword(credentialsId: 'oss-index-token', passwordVariable: 'NANCY_TOKEN', usernameVariable: 'NANCY_USER')]) {
// See: https://github.com/yarnpkg/yarn/issues/3254 sh '''docker build --pull --no-cache --squash --compress \\
sh '''docker run --rm \\ -t ${IMAGE}:ci-${BUILD_NUMBER} \\
-v "$(pwd)/backend:/app" \\ -f docker/Dockerfile \\
-v "$(pwd)/global:/app/global" \\ --build-arg TARGETPLATFORM=linux/amd64 \\
-w /app \\ --build-arg BUILDPLATFORM=linux/amd64 \\
node:latest \\ --build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\
sh -c "yarn install && yarn eslint . && rm -rf node_modules" --build-arg BUILD_VERSION="${BUILD_VERSION}" \\
''' --build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\
--build-arg SENTRY_DSN="${SENTRY_DSN:-}" \\
echo 'Docker Build ...' --build-arg GOPROXY="${GOPROXY:-}" \\
sh '''docker build --pull --no-cache --squash --compress \\ --build-arg GOPRIVATE="${GOPRIVATE:-}" \\
-t "${IMAGE}:ci-${BUILD_NUMBER}" \\ --build-arg NANCY_USER="${NANCY_USER}" \\
-f docker/Dockerfile \\ --build-arg NANCY_TOKEN="${NANCY_TOKEN}" \\
--build-arg TARGETPLATFORM=linux/amd64 \\ .
--build-arg BUILDPLATFORM=linux/amd64 \\ '''
--build-arg BUILD_VERSION="${BUILD_VERSION}" \\
--build-arg BUILD_COMMIT="${BUILD_COMMIT}" \\
--build-arg BUILD_DATE="$(date '+%Y-%m-%d %T %Z')" \\
.
'''
}
}
stage('Integration Tests Sqlite') {
steps {
// Bring up a stack
sh 'docker-compose up -d fullstack-sqlite'
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-sqlite) 120'
// Run tests
sh 'rm -rf test/results'
sh 'docker-compose up cypress-sqlite'
// Get results
sh 'docker cp -L "$(docker-compose ps -q cypress-sqlite):/test/results" test/'
}
post {
always {
// Dumps to analyze later
sh 'mkdir -p debug'
sh 'docker-compose logs fullstack-sqlite | gzip > debug/docker_fullstack_sqlite.log.gz'
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
// Cypress videos and screenshot artifacts
dir(path: 'test/results') {
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
}
junit 'test/results/junit/*'
} }
} }
} }
stage('Integration Tests Mysql') { stage('Test') {
when {
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
steps { steps {
// Bring up a stack // Bring up a stack
sh 'docker-compose up -d fullstack-mysql' sh 'docker-compose up -d fullstack'
sh './scripts/wait-healthy $(docker-compose ps -q fullstack-mysql) 120' sh './scripts/wait-healthy $(docker-compose ps -q fullstack) 120'
// Run tests // Run tests
sh 'rm -rf test/results' sh 'rm -rf test/results'
sh 'docker-compose up cypress-mysql' sh 'docker-compose up cypress'
// Get results // Get results
sh 'docker cp -L "$(docker-compose ps -q cypress-mysql):/test/results" test/' sh 'docker cp -L "$(docker-compose ps -q cypress):/test/results" test/'
} }
post { post {
always { always {
// Dumps to analyze later // Dumps to analyze later
sh 'mkdir -p debug' sh 'mkdir -p debug'
sh 'docker-compose logs fullstack-mysql | gzip > debug/docker_fullstack_mysql.log.gz' sh 'docker-compose logs fullstack | gzip > debug/docker_fullstack.log.gz'
sh 'docker-compose logs db | gzip > debug/docker_db.log.gz'
// Cypress videos and screenshot artifacts // Cypress videos and screenshot artifacts
dir(path: 'test/results') { dir(path: 'test/results') {
archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml' archiveArtifacts allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml'
@@ -148,6 +129,11 @@ pipeline {
sh 'yarn build' sh 'yarn build'
} }
// API Docs:
sh 'docker-compose exec -T fullstack curl -s --output /temp-docs/api-schema.json "http://fullstack:81/api/schema"'
sh 'mkdir -p "docs/.vuepress/dist/api"'
sh 'mv docs/api-schema.json docs/.vuepress/dist/api/'
dir(path: 'docs/.vuepress/dist') { dir(path: 'docs/.vuepress/dist') {
sh 'tar -czf ../../docs.tgz *' sh 'tar -czf ../../docs.tgz *'
} }
@@ -155,21 +141,29 @@ pipeline {
archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false) archiveArtifacts(artifacts: 'docs/docs.tgz', allowEmptyArchive: false)
} }
} }
/*
stage('MultiArch Build') { stage('MultiArch Build') {
when { when {
not { allOf {
equals expected: 'UNSTABLE', actual: currentBuild.result branch 'master'
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
} }
} }
steps { steps {
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([string(credentialsId: 'npm-sentry-dsn', variable: 'SENTRY_DSN')]) {
// Docker Login withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '${dpass}'" sh "docker login -u '${duser}' -p '${dpass}'"
// Buildx with push from cache // Buildx to local files
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}" // sh './scripts/buildx -o type=local,dest=docker-build'
// Buildx to with push
sh "./scripts/buildx --push ${BUILDX_PUSH_TAGS}"
}
} }
} }
} }
*/
stage('Docs Deploy') { stage('Docs Deploy') {
when { when {
allOf { allOf {
@@ -183,7 +177,7 @@ pipeline {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'npm-s3-docs', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) {
sh """docker run --rm \\ sh """docker run --rm \\
--name \${COMPOSE_PROJECT_NAME}-docs-upload \\ --name \${COMPOSE_PROJECT_NAME}-docs-upload \\
-e S3_BUCKET=jc21-npm-site \\ -e S3_BUCKET=$DOCS_BUCKET \\
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
-v \$(pwd):/app \\ -v \$(pwd):/app \\
@@ -197,7 +191,7 @@ pipeline {
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \\
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \\
jc21/ci-tools \\ jc21/ci-tools \\
aws cloudfront create-invalidation --distribution-id EN1G6DEWZUTDT --paths '/*' aws cloudfront create-invalidation --distribution-id $DOCS_CDN --paths '/*'
""" """
} }
} }
@@ -213,7 +207,40 @@ pipeline {
} }
steps { steps {
script { script {
def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.") def comment = pullRequest.comment("This is an automated message from CI:\n\nDocker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:git-3-${BRANCH_LOWER}`\n\n**Note:** ensure you backup your NPM instance before testing this PR image! Especially if this PR contains database changes.")
}
}
}
stage('Artifacts') {
when {
allOf {
branch 'master'
not {
equals expected: 'UNSTABLE', actual: currentBuild.result
}
}
}
steps {
sh 'mkdir -p artifacts'
// Docs
dir(path: 'docs/.vuepress/dist') {
sh 'zip -qr ../../../artifacts/docs.zip *'
}
// Multiarch builds
/*
dir(path: 'docker-build/linux_amd64/app') {
sh 'zip -qr ../../../artifacts/linux_amd64.zip *'
}
dir(path: 'docker-build/linux_arm64/app') {
sh 'zip -qr ../../../artifacts/linux_arm64.zip *'
}
dir(path: 'docker-build/linux_arm_v7/app') {
sh 'zip -qr ../../../artifacts/linux_arm_v7.zip *'
}
**/
// Archive them
dir(path: 'artifacts') {
archiveArtifacts artifacts: '**/*'
} }
} }
} }
@@ -221,8 +248,9 @@ pipeline {
post { post {
always { always {
sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30' sh 'docker-compose down --rmi all --remove-orphans --volumes -t 30'
sh './scripts/build-cleanup'
sh 'echo Reverting ownership' sh 'echo Reverting ownership'
sh 'docker run --rm -v $(pwd):/data jc21/ci-tools chown -R $(id -u):$(id -g) /data' sh 'docker run --rm -v $(pwd):/data node:latest chown -R "$(id -u):$(id -g)" /data'
} }
success { success {
juxtapose event: 'success' juxtapose event: 'success'

255
README.md
View File

@@ -1,7 +1,7 @@
<p align="center"> <p align="center">
<img src="https://nginxproxymanager.com/github.png"> <img src="https://nginxproxymanager.com/github.png">
<br><br> <br><br>
<img src="https://img.shields.io/badge/version-2.9.3-green.svg?style=for-the-badge"> <img src="https://img.shields.io/badge/version-3.0.0-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager"> <a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge"> <img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a> </a>
@@ -14,16 +14,12 @@
<a href="https://gitter.im/nginx-proxy-manager/community"> <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"> <img alt="Gitter" src="https://img.shields.io/gitter/room/nginx-proxy-manager/community?style=for-the-badge">
</a> </a>
<a href="https://reddit.com/r/nginxproxymanager">
<img alt="Reddit" src="https://img.shields.io/reddit/subreddit-subscribers/nginxproxymanager?label=Reddit%20Community&style=for-the-badge">
</a>
</p> </p>
This project comes as a pre-built docker image that enables you to easily forward to your websites This project comes as a pre-built docker image that enables you to easily forward to your websites
running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt. running at home or otherwise, including free SSL, without having to know too much about Nginx or Letsencrypt.
- [Quick Setup](#quick-setup) - [Quick Setup](https://nginxproxymanager.com#quick-setup)
- [Full Setup](https://nginxproxymanager.com/setup/) - [Full Setup](https://nginxproxymanager.com/setup/)
- [Screenshots](https://nginxproxymanager.com/screenshots/) - [Screenshots](https://nginxproxymanager.com/screenshots/)
@@ -56,67 +52,6 @@ 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) 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 4. Use the Nginx Proxy Manager as your gateway to forward to your other web based services
## Quick Setup
1. Install Docker and Docker-Compose
- [Docker Install documentation](https://docs.docker.com/install/)
- [Docker-Compose Install documentation](https://docs.docker.com/compose/install/)
2. Create a docker-compose.yml file similar to this:
```yml
version: '3'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
environment:
DB_MYSQL_HOST: "db"
DB_MYSQL_PORT: 3306
DB_MYSQL_USER: "npm"
DB_MYSQL_PASSWORD: "npm"
DB_MYSQL_NAME: "npm"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
db:
image: 'jc21/mariadb-aria:latest'
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: 'npm'
MYSQL_DATABASE: 'npm'
MYSQL_USER: 'npm'
MYSQL_PASSWORD: 'npm'
volumes:
- ./data/mysql:/var/lib/mysql
```
3. Bring up your stack
```bash
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.
## Contributors ## Contributors
@@ -128,43 +63,43 @@ Special thanks to the following contributors:
<tr> <tr>
<td align="center"> <td align="center">
<a href="https://github.com/Subv"> <a href="https://github.com/Subv">
<img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80" alt=""/> <img src="https://avatars1.githubusercontent.com/u/357072?s=460&u=d8adcdc91d749ae53e177973ed9b6bb6c4c894a3&v=4" width="80px;" alt=""/>
<br /><sub><b>Sebastian Valle</b></sub> <br /><sub><b>Sebastian Valle</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/Indemnity83"> <a href="https://github.com/Indemnity83">
<img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/35218?s=460&u=7082004ff35138157c868d7d9c683ccebfce5968&v=4" width="80px;" alt=""/>
<br /><sub><b>Kyle Klaus</b></sub> <br /><sub><b>Kyle Klaus</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/theraw"> <a href="https://github.com/theraw">
<img src="https://avatars1.githubusercontent.com/u/32969774?s=460&u=6b359971e15685fb0359e6a8c065a399b40dc228&v=4" width="80" alt=""/> <img src="https://avatars1.githubusercontent.com/u/32969774?s=460&u=6b359971e15685fb0359e6a8c065a399b40dc228&v=4" width="80px;" alt=""/>
<br /><sub><b>ƬHE ЯAW</b></sub> <br /><sub><b>ƬHE ЯAW</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/spalger"> <a href="https://github.com/spalger">
<img src="https://avatars2.githubusercontent.com/u/1329312?s=400&u=565223e38f1c052afb4c5dcca3fcf1c63ba17ae7&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/1329312?s=400&u=565223e38f1c052afb4c5dcca3fcf1c63ba17ae7&v=4" width="80px;" alt=""/>
<br /><sub><b>Spencer</b></sub> <br /><sub><b>Spencer</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/Xantios"> <a href="https://github.com/Xantios">
<img src="https://avatars3.githubusercontent.com/u/1507836?s=460&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/1507836?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Xantios Krugor</b></sub> <br /><sub><b>Xantios Krugor</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/dpanesso"> <a href="https://github.com/dpanesso">
<img src="https://avatars2.githubusercontent.com/u/2687121?s=460&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/2687121?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>David Panesso</b></sub> <br /><sub><b>David Panesso</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/IronTooch"> <a href="https://github.com/IronTooch">
<img src="https://avatars3.githubusercontent.com/u/27360514?s=460&u=69bf854a6647c55725f62ecb8d39249c6c0b2602&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/27360514?s=460&u=69bf854a6647c55725f62ecb8d39249c6c0b2602&v=4" width="80px;" alt=""/>
<br /><sub><b>IronTooch</b></sub> <br /><sub><b>IronTooch</b></sub>
</a> </a>
</td> </td>
@@ -172,43 +107,43 @@ Special thanks to the following contributors:
<tr> <tr>
<td align="center"> <td align="center">
<a href="https://github.com/damianog"> <a href="https://github.com/damianog">
<img src="https://avatars1.githubusercontent.com/u/2786682?s=460&u=76c6136fae797abb76b951cd8a246dcaecaf21af&v=4" width="80" alt=""/> <img src="https://avatars1.githubusercontent.com/u/2786682?s=460&u=76c6136fae797abb76b951cd8a246dcaecaf21af&v=4" width="80px;" alt=""/>
<br /><sub><b>Damiano</b></sub> <br /><sub><b>Damiano</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/tfmm"> <a href="https://github.com/tfmm">
<img src="https://avatars3.githubusercontent.com/u/6880538?s=460&u=ce0160821cc4aa802df8395200f2d4956a5bc541&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/6880538?s=460&u=ce0160821cc4aa802df8395200f2d4956a5bc541&v=4" width="80px;" alt=""/>
<br /><sub><b>Russ</b></sub> <br /><sub><b>Russ</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/margaale"> <a href="https://github.com/margaale">
<img src="https://avatars3.githubusercontent.com/u/20794934?s=460&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/20794934?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Marcelo Castagna</b></sub> <br /><sub><b>Marcelo Castagna</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/Steven-Harris"> <a href="https://github.com/Steven-Harris">
<img src="https://avatars2.githubusercontent.com/u/7720242?s=460&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/7720242?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Steven Harris</b></sub> <br /><sub><b>Steven Harris</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/jlesage"> <a href="https://github.com/jlesage">
<img src="https://avatars0.githubusercontent.com/u/1791123?s=460&v=4" width="80" alt=""/> <img src="https://avatars0.githubusercontent.com/u/1791123?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Jocelyn Le Sage</b></sub> <br /><sub><b>Jocelyn Le Sage</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/cmer"> <a href="https://github.com/cmer">
<img src="https://avatars0.githubusercontent.com/u/412?s=460&u=67dd8b2e3661bfd6f68ec1eaa5b9821bd8a321cd&v=4" width="80" alt=""/> <img src="https://avatars0.githubusercontent.com/u/412?s=460&u=67dd8b2e3661bfd6f68ec1eaa5b9821bd8a321cd&v=4" width="80px;" alt=""/>
<br /><sub><b>Carl Mercier</b></sub> <br /><sub><b>Carl Mercier</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/the1ts"> <a href="https://github.com/the1ts">
<img src="https://avatars1.githubusercontent.com/u/84956?s=460&v=4" width="80" alt=""/> <img src="https://avatars1.githubusercontent.com/u/84956?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Paul Mansfield</b></sub> <br /><sub><b>Paul Mansfield</b></sub>
</a> </a>
</td> </td>
@@ -216,43 +151,43 @@ Special thanks to the following contributors:
<tr> <tr>
<td align="center"> <td align="center">
<a href="https://github.com/OhHeyAlan"> <a href="https://github.com/OhHeyAlan">
<img src="https://avatars0.githubusercontent.com/u/11955126?s=460&u=fbaa5a1a4f73ef8960132c703349bfd037fe2630&v=4" width="80" alt=""/> <img src="https://avatars0.githubusercontent.com/u/11955126?s=460&u=fbaa5a1a4f73ef8960132c703349bfd037fe2630&v=4" width="80px;" alt=""/>
<br /><sub><b>OhHeyAlan</b></sub> <br /><sub><b>OhHeyAlan</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/dogmatic69"> <a href="https://github.com/dogmatic69">
<img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/94674?s=460&u=ca7647de53145c6283b6373ade5dc94ba99347db&v=4" width="80px;" alt=""/>
<br /><sub><b>Carl Sutton</b></sub> <br /><sub><b>Carl Sutton</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/tg44"> <a href="https://github.com/tg44">
<img src="https://avatars0.githubusercontent.com/u/31839?s=460&u=ad32f4cadfef5e5fb09cdfa4b7b7b36a99ba6811&v=4" width="80" alt=""/> <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> <br /><sub><b>Gergő Törcsvári</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/vrenjith"> <a href="https://github.com/vrenjith">
<img src="https://avatars3.githubusercontent.com/u/2093241?s=460&u=96ce93a9bebabdd0a60a2dc96cd093a41d5edaba&v=4" width="80" alt=""/> <img src="https://avatars3.githubusercontent.com/u/2093241?s=460&u=96ce93a9bebabdd0a60a2dc96cd093a41d5edaba&v=4" width="80px;" alt=""/>
<br /><sub><b>vrenjith</b></sub> <br /><sub><b>vrenjith</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/duhruh"> <a href="https://github.com/duhruh">
<img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/1133969?s=460&u=c0691e6131ec6d516416c1c6fcedb5034f877bbe&v=4" width="80px;" alt=""/>
<br /><sub><b>David Rivera</b></sub> <br /><sub><b>David Rivera</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/jipjan"> <a href="https://github.com/jipjan">
<img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/1384618?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Jaap-Jan de Wit</b></sub> <br /><sub><b>Jaap-Jan de Wit</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/jmwebslave"> <a href="https://github.com/jmwebslave">
<img src="https://avatars2.githubusercontent.com/u/6118262?s=460&u=7db409c47135b1e141c366bbb03ed9fae6ac2638&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/6118262?s=460&u=7db409c47135b1e141c366bbb03ed9fae6ac2638&v=4" width="80px;" alt=""/>
<br /><sub><b>James Morgan</b></sub> <br /><sub><b>James Morgan</b></sub>
</a> </a>
</td> </td>
@@ -260,160 +195,22 @@ Special thanks to the following contributors:
<tr> <tr>
<td align="center"> <td align="center">
<a href="https://github.com/chaptergy"> <a href="https://github.com/chaptergy">
<img src="https://avatars2.githubusercontent.com/u/26956711?s=460&u=7d9adebabb6b4e7af7cb05d98d751087a372304b&v=4" width="80" alt=""/> <img src="https://avatars2.githubusercontent.com/u/26956711?s=460&u=7d9adebabb6b4e7af7cb05d98d751087a372304b&v=4" width="80px;" alt=""/>
<br /><sub><b>chaptergy</b></sub> <br /><sub><b>chaptergy</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/Philip-Mooney"> <a href="https://github.com/Philip-Mooney">
<img src="https://avatars0.githubusercontent.com/u/48624631?s=460&v=4" width="80" alt=""/> <img src="https://avatars0.githubusercontent.com/u/48624631?s=460&v=4" width="80px;" alt=""/>
<br /><sub><b>Philip Mooney</b></sub> <br /><sub><b>Philip Mooney</b></sub>
</a> </a>
</td> </td>
<td align="center"> <td align="center">
<a href="https://github.com/WaterCalm"> <a href="https://github.com/WaterCalm">
<img src="https://avatars1.githubusercontent.com/u/23502129?s=400&v=4" width="80" alt=""/> <img src="https://avatars1.githubusercontent.com/u/23502129?s=400&v=4" width="80px;" alt=""/>
<br /><sub><b>WaterCalm</b></sub> <br /><sub><b>WaterCalm</b></sub>
</a> </a>
</td> </td>
<td align="center">
<a href="https://github.com/lebrou34">
<img src="https://avatars1.githubusercontent.com/u/16373103?s=460&v=4" width="80" alt=""/>
<br /><sub><b>lebrou34</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lightglitch">
<img src="https://avatars0.githubusercontent.com/u/196953?s=460&v=4" width="80" alt=""/>
<br /><sub><b>Mário Franco</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/klutchell">
<img src="https://avatars3.githubusercontent.com/u/20458272?s=460&v=4" width="80" alt=""/>
<br /><sub><b>Kyle Harding</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ahgraber">
<img src="https://avatars.githubusercontent.com/u/24922003?s=460&u=8376c9f00af9b6057ba4d2fb03b4f1b20a75277f&v=4" width="80" alt=""/>
<br /><sub><b>Alex Graber</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/MooBaloo">
<img src="https://avatars.githubusercontent.com/u/9493496?s=460&v=4" width="80" alt=""/>
<br /><sub><b>MooBaloo</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Shuro">
<img src="https://avatars.githubusercontent.com/u/944030?s=460&v=4" width="80" alt=""/>
<br /><sub><b>Shuro</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lorisbergeron">
<img src="https://avatars.githubusercontent.com/u/51918567?s=460&u=778e4ff284b7d7304450f98421c99f79298371fb&v=4" width="80" alt=""/>
<br /><sub><b>Loris Bergeron</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/hepelayo">
<img src="https://avatars.githubusercontent.com/u/8243119?v=4" width="80" alt=""/>
<br /><sub><b>hepelayo</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jonasled">
<img src="https://avatars.githubusercontent.com/u/46790650?v=4" width="80" alt=""/>
<br /><sub><b>Jonas Leder</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/stegmannb">
<img src="https://avatars.githubusercontent.com/u/12850482?v=4" width="80" alt=""/>
<br /><sub><b>Bastian Stegmann</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Stealthii">
<img src="https://avatars.githubusercontent.com/u/998920?v=4" width="80" alt=""/>
<br /><sub><b>Stealthii</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/thegamingninja">
<img src="https://avatars.githubusercontent.com/u/8020534?v=4" width="80" alt=""/>
<br /><sub><b>THEGamingninja</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/italobb">
<img src="https://avatars.githubusercontent.com/u/1801687?v=4" width="80" alt=""/>
<br /><sub><b>Italo Borssatto</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/GurjinderSingh">
<img src="https://avatars.githubusercontent.com/u/3470709?v=4" width="80" alt=""/>
<br /><sub><b>Gurjinder Singh</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/phantomski77">
<img src="https://avatars.githubusercontent.com/u/69464125?v=4" width="80" alt=""/>
<br /><sub><b>David Dosoudil</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ijaron">
<img src="https://avatars.githubusercontent.com/u/5156472?v=4" width="80" alt=""/>
<br /><sub><b>ijaron</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/nielscil">
<img src="https://avatars.githubusercontent.com/u/9073152?v=4" width="80" alt=""/>
<br /><sub><b>Niels Bouma</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ogarai">
<img src="https://avatars.githubusercontent.com/u/2949572?v=4" width="80" alt=""/>
<br /><sub><b>Orko Garai</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center">
<a href="https://github.com/baruffaldi">
<img src="https://avatars.githubusercontent.com/u/36949?v=4" width="80" alt=""/>
<br /><sub><b>Filippo Baruffaldi</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/bikram990">
<img src="https://avatars.githubusercontent.com/u/6782131?v=4" width="80" alt=""/>
<br /><sub><b>Bikramjeet Singh</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/razvanstoica89">
<img src="https://avatars.githubusercontent.com/u/28236583?v=4" width="80" alt=""/>
<br /><sub><b>Razvan Stoica</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/psharma04">
<img src="https://avatars.githubusercontent.com/u/22587474?v=4" width="80" alt=""/>
<br /><sub><b>RBXII3</b></sub>
</a>
</td>
</tr> </tr>
</table> </table>
<!-- markdownlint-enable --> <!-- markdownlint-enable -->

8
backend/.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false

View File

@@ -1,73 +0,0 @@
{
"env": {
"node": true,
"es6": true
},
"extends": [
"eslint:recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"align-assignments"
],
"rules": {
"arrow-parens": [
"error",
"always"
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"key-spacing": [
"error",
{
"align": "value"
}
],
"comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"func-call-spacing": [
"error",
"never"
],
"keyword-spacing": [
"error",
{
"before": true
}
],
"no-irregular-whitespace": "error",
"no-unused-expressions": 0,
"align-assignments/align-assignments": [
2,
{
"requiresOnly": false
}
]
}
}

8
backend/.gitignore vendored
View File

@@ -1,8 +0,0 @@
config/development.json
data/*
yarn-error.log
tmp
certbot.log
node_modules
core.*

92
backend/.golangci.yml Normal file
View File

@@ -0,0 +1,92 @@
linters:
enable:
# Prevents against memory leaks in production caused by not closing file handle
- bodyclose
# Detects unused declarations in a go package
- deadcode
# Detects cloned code. DRY is good programming practice. Can cause issues with testing code where
# simplicity is preferred over duplication. Disabled for test code.
#- dupl
# Detects unchecked errors in go programs. These unchecked errors can be critical bugs in some cases.
- errcheck
# Simplifies go code.
- gosimple
# Reports suspicious constructs, maintained by goteam. e.g. Printf unused params not caught
# at compile time.
- govet
# Detect security issues with gocode. Use of secrets in code or obsolete security algorithms.
# It's imaged heuristic methods are used in finding problems. If issues with rules are found
# particular rules can be disabled as required.
# Could possibility cause issues with testing. Disabled for test code.
- gosec
# Detect repeated strings that could be replaced by a constant
- goconst
# Misc linters missing from other projects. Grouped into 3 categories diagnostics, style
# and performance
- gocritic
# Limits code cyclomatic complexity
- gocyclo
# Detects if code needs to be gofmt'd
- gofmt
# Detects unused go package imports
- goimports
# Detcts style mistakes not correctness. Golint is meant to carry out the
# stylistic conventions put forth in Effective Go and CodeReviewComments.
# golint has false positives and false negatives and can be tweaked.
- golint
# Detects ineffectual assignments in code
- ineffassign
# Detect commonly misspelled english words in comments
- misspell
# Detect naked returns on non-trivial functions, and conform with Go CodeReviewComments
- nakedret
# Detect slice allocations that can be preallocated
- prealloc
# Misc collection of static analysis tools
- staticcheck
# Detects unused struct fields
- structcheck
# Parses and typechecks the code like the go compiler
- typecheck
# Detects unused constants, variables, functions and types
- unused
# Detects unused global variables and constants
- varcheck
# Remove unnecessary type conversions
- unconvert
# Remove unnecessary(unused) function parameters
- unparam
linters-settings:
goconst:
# minimal length of string constant
# default: 3
min-len: 2
# minimum number of occurrences of string constant
# default: 3
min-occurences: 2
misspell:
locale: UK
ignore-words:
- color
issues:
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
# We have chosen an arbitrary value that works based on practical usage.
max-same: 20
# See cmdline flag documentation for more info about default excludes --exclude-use-default
# Nothing is excluded by default
exclude-use-default: false
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# Exclude some linters from running on tests files. # TODO: Add examples why this is good
- path: _test\.go
linters:
# Tests should be simple? Add example why this is good?
- gocyclo
# Error checking adds verbosity and complexity for minimal value
- errcheck
# Table test encourage duplication in defining the table tests.
- dupl
# Hard coded example tokens, SQL injection and other bad practices may
# want to be tested
- gosec

22
backend/.nancy-ignore Normal file
View File

@@ -0,0 +1,22 @@
# If you need to ignore any of nancy's warnings add them
# here with a reference to the package/version that
# triggers them and rational for ignoring it.
# pkg:golang/github.com/coreos/etcd@3.3.10
# etcd before versions 3.3.23 and 3.4.10 does not perform any password length validation
CVE-2020-15115
# pkg:golang/github.com/coreos/etcd@3.3.10
# In ectd before versions 3.4.10 and 3.3.23, gateway TLS authentication is only applied to endpoints detected in DNS SRV records
CVE-2020-15136
# pkg:golang/github.com/coreos/etcd@3.3.10
# In etcd before versions 3.3.23 and 3.4.10, the etcd gateway is a simple TCP proxy to allow for basic service discovery and access
CVE-2020-15114
# pkg:golang/github.com/gorilla/websocket@1.4.0
# Integer Overflow or Wraparound
CWE-190
# jwt-go before 4.0.0-preview1 allows attackers to bypass intended access restrict...
CVE-2020-26160

View File

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

56
backend/Taskfile.yml Normal file
View File

@@ -0,0 +1,56 @@
version: '2'
tasks:
default:
cmds:
- task: run
run:
desc: Build and run
sources:
- internal/**/*.go
- cmd/**/*.go
cmds:
- task: build
- cmd: echo -e "==> Running..."
silent: true
- cmd: ../dist/bin/server
ignore_error: true
silent: true
env:
LOG_LEVEL: debug
build:
desc: Build the server
cmds:
- cmd: echo -e "==> Building..."
silent: true
- cmd: rm -f dist/bin/*
silent: true
- cmd: go build -ldflags="-X main.commit={{.GIT_COMMIT}} -X main.version={{.VERSION}}" -o ../dist/bin/server ./cmd/server/main.go
silent: true
- task: lint
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
VERSION:
sh: cat ../.version
env:
GO111MODULE: on
CGO_ENABLED: 1
lint:
desc: Linting
cmds:
- cmd: echo -e "==> Linting..."
silent: true
- cmd: bash scripts/lint.sh
silent: true
test:
desc: Testing
cmds:
- cmd: echo -e "==> Testing..."
silent: true
- cmd: bash scripts/test.sh
silent: true

View File

@@ -1,90 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const fileUpload = require('express-fileupload');
const compression = require('compression');
const log = require('./logger').express;
/**
* App
*/
const app = express();
app.use(fileUpload());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
// Gzip
app.use(compression());
/**
* General Logging, BEFORE routes
*/
app.disable('x-powered-by');
app.enable('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.enable('strict routing');
// pretty print JSON when not live
if (process.env.NODE_ENV !== 'production') {
app.set('json spaces', 2);
}
// CORS for everything
app.use(require('./lib/express/cors'));
// General security/cache related headers + server header
app.use(function (req, res, next) {
let x_frame_options = 'DENY';
if (typeof process.env.X_FRAME_OPTIONS !== 'undefined' && process.env.X_FRAME_OPTIONS) {
x_frame_options = process.env.X_FRAME_OPTIONS;
}
res.set({
'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(require('./lib/express/jwt')());
app.use('/', require('./routes/api/main'));
// production error handler
// no stacktraces leaked to user
// eslint-disable-next-line
app.use(function (err, req, res, next) {
let payload = {
error: {
code: err.status,
message: err.public ? err.message : 'Internal Error'
}
};
if (process.env.NODE_ENV === 'development' || (req.baseUrl + req.path).includes('nginx/certificates')) {
payload.debug = {
stack: typeof err.stack !== 'undefined' && err.stack ? err.stack.split('\n') : null,
previous: err.previous
};
}
// Not every error is worth logging - but this is good for now until it gets annoying.
if (typeof err.stack !== 'undefined' && err.stack) {
if (process.env.NODE_ENV === 'development') {
log.debug(err.stack);
} else if (typeof err.public == 'undefined' || !err.public) {
log.warn(err.message);
}
}
res
.status(err.status || 500)
.send(payload);
});
module.exports = app;

View File

@@ -0,0 +1,45 @@
package main
import (
"os"
"os/signal"
"syscall"
"npm/internal/api"
"npm/internal/config"
"npm/internal/database"
"npm/internal/entity/setting"
"npm/internal/logger"
"npm/internal/state"
"npm/internal/worker"
)
var commit string
var version string
var sentryDSN string
func main() {
config.Init(&version, &commit, &sentryDSN)
appstate := state.NewState()
setting.ApplySettings()
database.CheckSetup()
go worker.StartCertificateWorker(appstate)
api.StartServer()
irqchan := make(chan os.Signal, 1)
signal.Notify(irqchan, syscall.SIGINT, syscall.SIGTERM)
for irq := range irqchan {
if irq == syscall.SIGINT || irq == syscall.SIGTERM {
logger.Info("Got ", irq, " shutting server down ...")
// Close db
err := database.GetInstance().Close()
if err != nil {
logger.Error("DatabaseCloseError", err)
}
break
}
}
}

View File

@@ -1,2 +0,0 @@
These files are use in development and are not deployed as part of the final product.

View File

@@ -1,10 +0,0 @@
{
"database": {
"engine": "mysql",
"host": "db",
"name": "npm",
"user": "npm",
"password": "npm",
"port": 3306
}
}

View File

@@ -1,26 +0,0 @@
{
"database": {
"engine": "knex-native",
"knex": {
"client": "sqlite3",
"connection": {
"filename": "/app/config/mydb.sqlite"
},
"pool": {
"min": 0,
"max": 1,
"createTimeoutMillis": 3000,
"acquireTimeoutMillis": 30000,
"idleTimeoutMillis": 30000,
"reapIntervalMillis": 1000,
"createRetryIntervalMillis": 100,
"propagateCreateError": false
},
"migrations": {
"tableName": "migrations",
"stub": "src/backend/lib/migrate_template.js",
"directory": "src/backend/migrations"
}
}
}
}

View File

@@ -1,33 +0,0 @@
const config = require('config');
if (!config.has('database')) {
throw new Error('Database config does not exist! Please read the instructions: https://github.com/jc21/nginx-proxy-manager/blob/master/doc/INSTALL.md');
}
function generateDbConfig() {
if (config.database.engine === 'knex-native') {
return config.database.knex;
} else
return {
client: config.database.engine,
connection: {
host: config.database.host,
user: config.database.user,
password: config.database.password,
database: config.database.name,
port: config.database.port
},
migrations: {
tableName: 'migrations'
}
};
}
let data = generateDbConfig();
if (typeof config.database.version !== 'undefined') {
data.version = config.database.version;
}
module.exports = require('knex')(data);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "CertificateAuthorityList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CertificateAuthorityObject"
}
}
}
}

View File

@@ -0,0 +1,36 @@
{
"type": "object",
"description": "CertificateAuthorityObject",
"additionalProperties": false,
"required": [
"id",
"created_on",
"modified_on",
"name",
"acme2_url"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"acme2_url": {
"type": "string",
"minLength": 8,
"maxLength": 255
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "CertificateList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CertificateObject"
}
}
}
}

View File

@@ -0,0 +1,82 @@
{
"type": "object",
"description": "CertificateObject",
"additionalProperties": false,
"required": [
"id",
"created_on",
"modified_on",
"expires_on",
"type",
"user_id",
"certificate_authority_id",
"dns_provider_id",
"name",
"status",
"domain_names"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"expires_on": {
"type": "integer",
"minimum": 1,
"nullable": true
},
"type": {
"type": "string",
"enum": [
"custom",
"http",
"dns"
]
},
"user_id": {
"type": "integer",
"minimum": 1
},
"certificate_authority_id": {
"type": "integer",
"minimum": 0
},
"dns_provider_id": {
"type": "integer",
"minimum": 0
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"domain_names": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 4
}
},
"status": {
"type": "string",
"enum": [
"ready",
"requesting",
"failed",
"provided"
]
},
"error_message": {
"type": "string"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"type": "object",
"description": "ConfigObject"
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "DNSProviderList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DNSProviderObject"
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "DNSProviderObject",
"additionalProperties": false,
"required": [
"id",
"created_on",
"modified_on",
"user_id",
"provider_key",
"name",
"meta"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"user_id": {
"type": "integer",
"minimum": 1
},
"provider_key": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"meta": {
"type": "object"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"type": "object",
"description": "DeletedItemResponse",
"additionalProperties": false,
"required": [
"result"
],
"properties": {
"result": {
"type": "boolean",
"nullable": true
},
"error": {
"$ref": "#/components/schemas/ErrorObject"
}
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "object",
"description": "ErrorObject",
"additionalProperties": false,
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"description": "Error code",
"minimum": 0
},
"message": {
"type": "string",
"description": "Error message"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "object",
"description": "FilterObject",
"additionalProperties": false,
"required": [
"field",
"modifier",
"value"
],
"properties": {
"field": {
"type": "string",
"description": "Field to filter with"
},
"modifier": {
"type": "string",
"description": "Filter modifier",
"pattern": "^(equals|not|min|max|greater|lesser|contains|starts|ends|in|notin)$"
},
"value": {
"type": "array",
"description": "Values used for filtering",
"items": {
"type": "string"
}
}
}
}

View File

@@ -0,0 +1,41 @@
{
"type": "object",
"description": "HealthObject",
"additionalProperties": false,
"required": [
"version",
"commit",
"healthy",
"setup",
"error_reporting"
],
"properties": {
"version": {
"type": "string",
"description": "Version",
"example": "3.0.0",
"minLength": 1
},
"commit": {
"type": "string",
"description": "Commit hash",
"example": "946b88f",
"minLength": 7
},
"healthy": {
"type": "boolean",
"description": "Healthy?",
"example": true
},
"setup": {
"type": "boolean",
"description": "Is the application set up?",
"example": true
},
"error_reporting": {
"type": "boolean",
"description": "Will the application send any error reporting?",
"example": true
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "HostList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HostObject"
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"type": "object",
"description": "HostObject",
"additionalProperties": false,
"required": [
"id",
"created_on",
"modified_on",
"expires_on",
"user_id",
"provider",
"name",
"domain_names"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"expires_on": {
"type": "integer",
"minimum": 1
},
"user_id": {
"type": "integer",
"minimum": 1
},
"provider": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"domain_names": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 4
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "SettingList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SettingObject"
}
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "SettingObject",
"additionalProperties": false,
"required": [
"id",
"name",
"value"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 100
},
"value": {
"oneOf": [
{
"type": "array"
},
{
"type": "boolean"
},
{
"type": "object"
},
{
"type": "integer"
}
]
}
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "object",
"description": "SortObject",
"additionalProperties": false,
"required": [
"field",
"direction"
],
"properties": {
"field": {
"type": "string",
"description": "Field for sorting on"
},
"direction": {
"type": "string",
"description": "Sort order",
"pattern": "^(ASC|DESC)$"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "StreamList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/StreamObject"
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"type": "object",
"description": "StreamObject",
"additionalProperties": false,
"required": [
"id",
"created_on",
"modified_on",
"expires_on",
"user_id",
"provider",
"name",
"domain_names"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"expires_on": {
"type": "integer",
"minimum": 1
},
"user_id": {
"type": "integer",
"minimum": 1
},
"provider": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"domain_names": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 4
}
}
}
}

View File

@@ -0,0 +1,22 @@
{
"type": "object",
"description": "TokenObject",
"additionalProperties": false,
"required": [
"expires",
"token"
],
"properties": {
"expires": {
"type": "number",
"description": "Token Expiry Unix Time",
"example": 1566540249,
"minimum": 1
},
"token": {
"type": "string",
"description": "JWT Token",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
}

View File

@@ -0,0 +1,45 @@
{
"type": "object",
"description": "UserList",
"additionalProperties": false,
"required": [
"total",
"offset",
"limit",
"sort"
],
"properties": {
"total": {
"type": "integer",
"description": "Total number of rows"
},
"offset": {
"type": "integer",
"description": "Pagination Offset"
},
"limit": {
"type": "integer",
"description": "Pagination Limit"
},
"sort": {
"type": "array",
"description": "Sorting",
"items": {
"$ref": "#/components/schemas/SortObject"
}
},
"filter": {
"type": "array",
"description": "Filters",
"items": {
"$ref": "#/components/schemas/FilterObject"
}
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserObject"
}
}
}
}

View File

@@ -0,0 +1,75 @@
{
"type": "object",
"description": "UserObject",
"additionalProperties": false,
"required": [
"id",
"name",
"nickname",
"email",
"created_on",
"modified_on",
"roles",
"is_disabled"
],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"name": {
"type": "string",
"minLength": 2,
"maxLength": 100
},
"nickname": {
"type": "string",
"minLength": 2,
"maxLength": 100
},
"email": {
"type": "string",
"minLength": 5,
"maxLength": 150
},
"created_on": {
"type": "integer",
"minimum": 1
},
"modified_on": {
"type": "integer",
"minimum": 1
},
"roles": {
"type": "array",
"items": {
"type": "string",
"minLength": 2
}
},
"gravatar_url": {
"type": "string"
},
"is_disabled": {
"type": "boolean"
},
"is_deleted": {
"type": "boolean"
},
"auth": {
"type": "object",
"required": [
"type"
],
"properties": {
"id": {
"type": "integer"
},
"type": {
"type": "string",
"pattern": "^password$"
}
}
}
}
}

9
backend/doc/main.go Normal file
View File

@@ -0,0 +1,9 @@
package doc
import "embed"
// SwaggerFiles contain all the files used for swagger schema generation
//go:embed api.swagger.json
//go:embed components
//go:embed paths
var SwaggerFiles embed.FS

View File

@@ -0,0 +1,39 @@
{
"operationId": "deleteCertificateAuthority",
"summary": "Delete a Certificate Authority",
"tags": [
"Certificate Authorities"
],
"parameters": [
{
"in": "path",
"name": "caID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the Certificate Authority",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
{
"operationId": "getCertificateAuthority",
"summary": "Get a Certificate Authority object by ID",
"tags": [
"Certificate Authorities"
],
"parameters": [
{
"in": "path",
"name": "caID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Certificate Authority",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateAuthorityObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1602588511,
"modified_on": 1602588511,
"name": "Let's Encrypt",
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
{
"operationId": "updateCertificateAuthority",
"summary": "Update an existing Certificate Authority",
"tags": [
"Certificate Authorities"
],
"parameters": [
{
"in": "path",
"name": "caID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Certificate Authority",
"example": 1
}
],
"requestBody": {
"description": "Certificate Authority details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateCertificateAuthority}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateAuthorityObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1602588511,
"modified_on": 1602588511,
"name": "Let's Encrypt",
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"operationId": "getCertificateAuthorities",
"summary": "Get a list of Certificate Authorities",
"tags": [
"Certificate Authorities"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateAuthorityList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 2,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
{
"id": 1,
"created_on": 1602588511,
"modified_on": 1602588511,
"name": "Let's Encrypt",
"acme2_url": "https://acme-v02.api.letsencrypt.org/directory"
},
{
"id": 2,
"created_on": 1602588511,
"modified_on": 1602588511,
"name": "Let's Encrypt (Staging)",
"acme2_url": "https://acme-staging-v02.api.letsencrypt.org/directory"
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,48 @@
{
"operationId": "createCertificateAuthority",
"summary": "Create a new Certificate Authority",
"tags": [
"Certificate Authorities"
],
"requestBody": {
"description": "Certificate Authority to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateCertificateAuthority}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateAuthorityObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 3,
"created_on": 1602588900,
"modified_on": 1602588900,
"name": "Boulder",
"acme2_url": "https://boulder.local/directory"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "deleteCertificate",
"summary": "Delete a Certificate",
"tags": [
"Certificates"
],
"parameters": [
{
"in": "path",
"name": "certificateID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the certificate",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot delete a certificate that is in use!"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "getCertificate",
"summary": "Get a certificate object by ID",
"tags": [
"Certificates"
],
"parameters": [
{
"in": "path",
"name": "certificateID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the certificate",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1604536109,
"modified_on": 1604536109,
"expires_on": null,
"type": "dns",
"user_id": 1,
"certificate_authority_id": 2,
"dns_provider_id": 1,
"name": "test1.jc21.com.au",
"domain_names": [
"test1.jc21.com.au"
],
"status": "ready"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,69 @@
{
"operationId": "updateCertificate",
"summary": "Update an existing Certificate",
"tags": [
"Certificates"
],
"parameters": [
{
"in": "path",
"name": "certificateID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the certificate",
"example": 1
}
],
"requestBody": {
"description": "Certificate details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateCertificate}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1604536109,
"modified_on": 1604536109,
"expires_on": null,
"type": "dns",
"user_id": 1,
"certificate_authority_id": 2,
"dns_provider_id": 1,
"name": "test1.jc21.com.au",
"domain_names": [
"test1.jc21.com.au"
],
"status": "ready"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
{
"operationId": "getCertificates",
"summary": "Get a list of certificates",
"tags": [
"Certificates"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 1,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
{
"id": 1,
"created_on": 1604536109,
"modified_on": 1604536109,
"expires_on": null,
"type": "dns",
"user_id": 1,
"certificate_authority_id": 2,
"dns_provider_id": 1,
"name": "test1.jc21.com.au",
"domain_names": [
"test1.jc21.com.au"
],
"status": "ready"
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
{
"operationId": "createCertificate",
"summary": "Create a new Certificate",
"tags": [
"Certificates"
],
"requestBody": {
"description": "Certificate to create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateCertificate}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/CertificateObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1604536109,
"modified_on": 1604536109,
"expires_on": null,
"type": "dns",
"user_id": 1,
"certificate_authority_id": 2,
"dns_provider_id": 1,
"name": "test1.jc21.com.au",
"domain_names": [
"test1.jc21.com.au"
],
"status": "ready"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
{
"operationId": "config",
"summary": "Returns the API Service configuration",
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/ConfigObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"data": "/data",
"log": {
"level": "debug",
"format": "nice"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,87 @@
{
"operationId": "getDNSProviders",
"summary": "Get a list of DNS Providers",
"tags": [
"DNS Providers"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/DNSProviderList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 1,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
{
"id": 1,
"created_on": 1602593653,
"modified_on": 1602593653,
"user_id": 1,
"provider_key": "route53",
"name": "Route53",
"meta": {
"access_key": "abc123",
"access_secret": "def098",
"zone_id": "ABC123"
}
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
{
"operationId": "createDNSProvider",
"summary": "Create a new DNS Provider",
"tags": [
"DNS Providers"
],
"requestBody": {
"description": "DNS Provider to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateDNSProvider}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/DNSProviderObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1602593653,
"modified_on": 1602593653,
"user_id": 1,
"provider_key": "route53",
"name": "Route53",
"meta": {
"access_key": "abc123",
"access_secret": "def098",
"zone_id": "ABC123"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "deleteDNSProvider",
"summary": "Delete a DNS Provider",
"tags": [
"DNS Providers"
],
"parameters": [
{
"in": "path",
"name": "providerID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the DNS Provider",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot delete a DNS Provider that is in use!"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
{
"operationId": "getDNSProvider",
"summary": "Get a DNS Provider object by ID",
"tags": [
"DNS Providers"
],
"parameters": [
{
"in": "path",
"name": "providerID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the DNS Provider",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/DNSProviderObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"created_on": 1602593653,
"modified_on": 1602593653,
"user_id": 1,
"provider_key": "route53",
"name": "Route53",
"meta": {
"access_key": "abc123",
"access_secret": "def098",
"zone_id": "ABC123"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,69 @@
{
"operationId": "updateDNSProvider",
"summary": "Update an existing DNS Provider",
"tags": [
"DNS Providers"
],
"parameters": [
{
"in": "path",
"name": "providerID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the DNS Provider",
"example": 1
}
],
"requestBody": {
"description": "DNS Provider details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateDNSProvider}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/DNSProviderObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"result": {
"id": 1,
"created_on": 1602593653,
"modified_on": 1602593653,
"user_id": 1,
"provider_key": "route53",
"name": "Route53",
"meta": {
"access_key": "abc123",
"access_secret": "def098",
"zone_id": "ABC123"
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,47 @@
{
"operationId": "health",
"summary": "Returns the API health status",
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/HealthObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"version": "3.0.0",
"commit": "9f119b6",
"healthy": true,
"setup": true,
"error_reporting": true
}
}
},
"unhealthy": {
"value": {
"result": {
"version": "3.0.0",
"commit": "9f119b6",
"healthy": false,
"setup": true,
"error_reporting": true
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
{
"operationId": "getHosts",
"summary": "Get a list of Hosts",
"tags": [
"Hosts"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/HostList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 1,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
"TODO"
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "deleteHost",
"summary": "Delete a Host",
"tags": [
"Hosts"
],
"parameters": [
{
"in": "path",
"name": "hostID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the Host",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot delete a host that is in use!"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
{
"operationId": "getHost",
"summary": "Get a Host object by ID",
"tags": [
"Hosts"
],
"parameters": [
{
"in": "path",
"name": "hostID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Host",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/HostObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"operationId": "updateHost",
"summary": "Update an existing Host",
"tags": [
"Hosts"
],
"parameters": [
{
"in": "path",
"name": "hostID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Host",
"example": 1
}
],
"requestBody": {
"description": "Host details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateHost}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/HostObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
{
"operationId": "createHost",
"summary": "Create a new Host",
"tags": [
"Hosts"
],
"requestBody": {
"description": "Host to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateHost}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/HostObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"operationId": "schema",
"summary": "Returns this swagger API schema",
"responses": {
"200": {
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,84 @@
{
"operationId": "getSettings",
"summary": "Get a list of settings",
"tags": [
"Settings"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/SettingList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 1,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
{
"id": 1,
"created_on": 1578010090,
"modified_on": 1578010095,
"name": "default-site",
"value": {
"html": "<p>not found</p>",
"type": "custom"
}
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"operationId": "getSetting",
"summary": "Get a setting object by name",
"tags": [
"Settings"
],
"parameters": [
{
"in": "path",
"name": "name",
"schema": {
"type": "string",
"minLength": 2
},
"required": true,
"description": "Name of the setting",
"example": "default-site"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/SettingObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 2,
"created_on": 1578010090,
"modified_on": 1578010095,
"name": "default-site",
"value": {
"html": "<p>not found</p>",
"type": "custom"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
{
"operationId": "updateSetting",
"summary": "Update an existing Setting",
"tags": [
"Settings"
],
"parameters": [
{
"in": "path",
"name": "name",
"schema": {
"type": "string",
"minLength": 2
},
"required": true,
"description": "Name of the setting",
"example": "default-site"
}
],
"requestBody": {
"description": "Setting details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateSetting}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/SettingObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 2,
"created_on": 1578010090,
"modified_on": 1578010090,
"name": "default-site",
"value": {
"html": "<p>not found</p>",
"type": "custom"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
{
"operationId": "createSetting",
"summary": "Create a new Setting",
"tags": [
"Settings"
],
"requestBody": {
"description": "Setting to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateSetting}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/SettingObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 2,
"created_on": 1578010090,
"modified_on": 1578010090,
"name": "default-site",
"value": {
"html": "<p>not found</p>",
"type": "custom"
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
{
"operationId": "getStreams",
"summary": "Get a list of Streams",
"tags": [
"Streams"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "id,name.asc,value.desc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 1,
"offset": 0,
"limit": 10,
"sort": [
{
"field": "name",
"direction": "ASC"
}
],
"items": [
"TODO"
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
{
"operationId": "createStream",
"summary": "Create a new Stream",
"tags": [
"Streams"
],
"requestBody": {
"description": "Host to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateStream}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "deleteStream",
"summary": "Delete a Stream",
"tags": [
"Streams"
],
"parameters": [
{
"in": "path",
"name": "streamID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the Stream",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot delete a Stream that is in use!"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,46 @@
{
"operationId": "getStream",
"summary": "Get a Stream object by ID",
"tags": [
"Streams"
],
"parameters": [
{
"in": "path",
"name": "streamID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Stream",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
{
"operationId": "updateStream",
"summary": "Update an existing Stream",
"tags": [
"Streams"
],
"parameters": [
{
"in": "path",
"name": "streamID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "ID of the Stream",
"example": 1
}
],
"requestBody": {
"description": "Stream details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateStream}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
{
"operationId": "refreshToken",
"summary": "Refresh your access token",
"tags": [
"Tokens"
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"scope": "user"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
{
"operationId": "requestToken",
"summary": "Request a new access token from credentials",
"tags": [
"Tokens"
],
"requestBody": {
"description": "Credentials Payload",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.GetToken}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/StreamObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"expires": 1566540510,
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"scope": "user"
}
}
}
}
}
}
},
"403": {
"description": "403 response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"error"
],
"properties": {
"result": {
"nullable": true
},
"error": {
"$ref": "#/components/schemas/ErrorObject"
}
}
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 403,
"message": "Not available during setup phase"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,121 @@
{
"operationId": "getUsers",
"summary": "Get a list of users",
"tags": [
"Users"
],
"parameters": [
{
"in": "query",
"name": "offset",
"schema": {
"type": "number"
},
"description": "The pagination row offset, default 0",
"example": 0
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "number"
},
"description": "The pagination row limit, default 10",
"example": 10
},
{
"in": "query",
"name": "sort",
"schema": {
"type": "string"
},
"description": "The sorting of the list",
"example": "name,nickname.desc,email.asc"
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/UserList"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"total": 3,
"offset": 0,
"limit": 100,
"sort": [
{
"field": "name",
"direction": "ASC"
},
{
"field": "nickname",
"direction": "DESC"
},
{
"field": "email",
"direction": "ASC"
}
],
"items": [
{
"id": 1,
"name": "Jamie Curnow",
"nickname": "James",
"email": "jc@jc21.com",
"created_on": 1578010090,
"modified_on": 1578010095,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false
},
{
"id": 2,
"name": "John Doe",
"nickname": "John",
"email": "johdoe@example.com",
"created_on": 1578010100,
"modified_on": 1578010105,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false
},
{
"id": 3,
"name": "Jane Doe",
"nickname": "Jane",
"email": "janedoe@example.com",
"created_on": 1578010110,
"modified_on": 1578010115,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false
}
]
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
{
"operationId": "createUser",
"summary": "Create a new User",
"tags": [
"Users"
],
"requestBody": {
"description": "User to Create",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.CreateUser}}"
}
}
},
"responses": {
"201": {
"description": "201 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/UserObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"name": "Jamie Curnow",
"nickname": "James",
"email": "jc@jc21.com",
"created_on": 1578010100,
"modified_on": 1578010100,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false,
"auth": {
"id": 1,
"type": "password"
}
}
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"required": [
"error"
],
"properties": {
"result": {
"nullable": true
},
"error": {
"$ref": "#/components/schemas/ErrorObject"
}
}
},
"examples": {
"default": {
"value": {
"error": {
"code": 400,
"message": "An user already exists with this email address"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
{
"operationId": "setPassword",
"summary": "Set a User's password",
"tags": [
"Users"
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"oneOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^me$"
}
]
},
"required": true,
"description": "Numeric ID of the user or 'me' to get yourself",
"example": 1
}
],
"requestBody": {
"description": "Credentials to set",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.SetAuth}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"type": "string"
}
}
},
"examples": {
"default": {
"value": {
"result": "TODO"
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
{
"operationId": "deleteUser",
"summary": "Delete a User",
"tags": [
"Users"
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "Numeric ID of the user",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": true
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeletedItemResponse"
},
"examples": {
"default": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot delete yourself!"
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
{
"operationId": "getUser",
"summary": "Get a user object by ID or 'me'",
"tags": [
"Users"
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"anyOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^me$"
}
]
},
"required": true,
"description": "Numeric ID of the user or 'me' to get yourself",
"example": 1
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/UserObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"name": "Jamie Curnow",
"nickname": "James",
"email": "jc@jc21.com",
"created_on": 1578010100,
"modified_on": 1578010105,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,119 @@
{
"operationId": "updateUser",
"summary": "Update an existing User",
"tags": [
"Users"
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"anyOf": [
{
"type": "integer",
"minimum": 1
},
{
"type": "string",
"pattern": "^me$"
}
]
},
"required": true,
"description": "Numeric ID of the user or 'me' to update yourself",
"example": 1
}
],
"requestBody": {
"description": "User details to update",
"required": true,
"content": {
"application/json": {
"schema": "{{schema.UpdateUser}}"
}
}
},
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"schema": {
"required": [
"result"
],
"properties": {
"result": {
"$ref": "#/components/schemas/UserObject"
}
}
},
"examples": {
"default": {
"value": {
"result": {
"id": 1,
"name": "Jamie Curnow",
"nickname": "James",
"email": "jc@jc21.com",
"created_on": 1578010100,
"modified_on": 1578010110,
"roles": [
"admin"
],
"gravatar_url": "https://www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?d=mm&r=pg&s=128",
"is_disabled": false,
"auth": {
"id": 1,
"type": "password"
}
}
}
}
}
}
}
},
"400": {
"description": "400 response",
"content": {
"application/json": {
"schema": {
"required": [
"error"
],
"properties": {
"result": {
"nullable": true
},
"error": {
"$ref": "#/components/schemas/ErrorObject"
}
}
},
"examples": {
"duplicateemail": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "A user already exists with this email address"
}
}
},
"nodisable": {
"value": {
"result": null,
"error": {
"code": 400,
"message": "You cannot disable yourself!"
}
}
}
}
}
}
}
}
}

20
backend/go.mod Normal file
View File

@@ -0,0 +1,20 @@
module npm
go 1.16
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e
github.com/fatih/color v1.10.0
github.com/getsentry/sentry-go v0.10.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.2.0
github.com/go-chi/jwtauth v4.0.4+incompatible
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760
github.com/jmoiron/sqlx v1.3.3
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/qri-io/jsonschema v0.2.1
github.com/stretchr/testify v1.7.0
github.com/vrischmann/envconfig v1.3.0
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
)

265
backend/go.sum Normal file
View File

@@ -0,0 +1,265 @@
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e h1:2R8DvYLNr5DL25eWwpOdPno1eIbTNjJC0d7v8ti5cus=
github.com/drexedam/gravatar v0.0.0-20210327211422-e94eea8c338e/go.mod h1:YjikoytuRI4q+GRd3xrOrKJN+Ayi2dwRomHLDDeMHfs=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g=
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth v4.0.4+incompatible h1:LGIxg6YfvSBzxU2BljXbrzVc1fMlgqSKBQgKOGAVtPY=
github.com/go-chi/jwtauth v4.0.4+incompatible/go.mod h1:Q5EIArY/QnD6BdS+IyDw7B2m6iNbnPxtfd6/BcmtWbs=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/jc21/jsref v0.0.0-20210608013137-43b07c7d31bd h1:Ag/L5Yc9BeBbi4i8bNAev8Ejtu/jq8Qk/xK+HDHnWNc=
github.com/jc21/jsref v0.0.0-20210608013137-43b07c7d31bd/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608014024-8bda7cb41eef h1:1jF5nv8PmgH2txfWGmsPium0Hj9PEnGkb96tkZ+4uDU=
github.com/jc21/jsref v0.0.0-20210608014024-8bda7cb41eef/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608014914-2edd4dea9791 h1:s0hsMFnTiGGytgwDbHo20OvmJj2/+FFMZvLpRNexnvk=
github.com/jc21/jsref v0.0.0-20210608014914-2edd4dea9791/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608023003-123d7fb98643 h1:ZpDTP4ow7hZMx0ORi06jnLP4ZDGQVa6SayH+5rWWlYg=
github.com/jc21/jsref v0.0.0-20210608023003-123d7fb98643/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608023437-810a57e5f736 h1:1nZYRLsHvECy8rbOLkqRBK45Y6zKQ5ZRuGPMQalPWVc=
github.com/jc21/jsref v0.0.0-20210608023437-810a57e5f736/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608024103-9eaa65f76123 h1:pb24Ybg78OdqO4GHh0xcwlVPWKlDYX/ZVnf+wq8D9To=
github.com/jc21/jsref v0.0.0-20210608024103-9eaa65f76123/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk=
github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kyoh86/richgo v0.3.8/go.mod h1:2C8POkF1H04iTOG2Tp1yyZhspCME9nN3cir3VXJ02II=
github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs=
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c h1:pGh5EFIfczeDHwgMHgfwjhZzL+8/E3uZF6T7vER/W8c=
github.com/lestrrat-go/jspointer v0.0.0-20181205001929-82fadba7561c/go.mod h1:xw2Gm4Mg+ST9s8fHR1VkUIyOJMJnSloRZlPQB+wyVpY=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/pdebug/v3 v3.0.1 h1:3G5sX/aw/TbMTtVc9U7IHBWRZtMvwvBziF1e4HoQtv8=
github.com/lestrrat-go/pdebug/v3 v3.0.1/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b h1:YUFRoeHK/mvRjBR0bBRDC7ZGygYchoQ8j1xMENlObro=
github.com/lestrrat-go/structinfo v0.0.0-20190212233437-acd51874663b/go.mod h1:s2U6PowV3/Jobkx/S9d0XiPwOzs6niW3DIouw+7nZC8=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,134 +0,0 @@
#!/usr/bin/env node
const logger = require('./logger').global;
async function appStart () {
// Create config file db settings if environment variables have been set
await createDbConfigFromEnvironment();
const migrate = require('./migrate');
const setup = require('./setup');
const app = require('./app');
const apiValidator = require('./lib/validator/api');
const internalCertificate = require('./internal/certificate');
const internalIpRanges = require('./internal/ip_ranges');
return migrate.latest()
.then(setup)
.then(() => {
return apiValidator.loadSchemas;
})
.then(internalIpRanges.fetch)
.then(() => {
internalCertificate.initTimer();
internalIpRanges.initTimer();
const server = app.listen(3000, () => {
logger.info('Backend PID ' + process.pid + ' listening on port 3000 ...');
process.on('SIGTERM', () => {
logger.info('PID ' + process.pid + ' received SIGTERM');
server.close(() => {
logger.info('Stopping.');
process.exit(0);
});
});
});
})
.catch((err) => {
logger.error(err.message);
setTimeout(appStart, 1000);
});
}
async function createDbConfigFromEnvironment() {
return new Promise((resolve, reject) => {
const envMysqlHost = process.env.DB_MYSQL_HOST || null;
const envMysqlPort = process.env.DB_MYSQL_PORT || null;
const envMysqlUser = process.env.DB_MYSQL_USER || null;
const envMysqlName = process.env.DB_MYSQL_NAME || null;
const envSqliteFile = process.env.DB_SQLITE_FILE || null;
if ((envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) || envSqliteFile) {
const fs = require('fs');
const filename = (process.env.NODE_CONFIG_DIR || './config') + '/' + (process.env.NODE_ENV || 'default') + '.json';
let configData = {};
try {
configData = require(filename);
} catch (err) {
// do nothing
}
if (configData.database && configData.database.engine && !configData.database.fromEnv) {
logger.info('Manual db configuration already exists, skipping config creation from environment variables');
resolve();
return;
}
if (envMysqlHost && envMysqlPort && envMysqlUser && envMysqlName) {
const newConfig = {
fromEnv: true,
engine: 'mysql',
host: envMysqlHost,
port: envMysqlPort,
user: envMysqlUser,
password: process.env.DB_MYSQL_PASSWORD,
name: envMysqlName,
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating MySQL db configuration from environment variables');
configData.database = newConfig;
} else {
const newConfig = {
fromEnv: true,
engine: 'knex-native',
knex: {
client: 'sqlite3',
connection: {
filename: envSqliteFile
},
useNullAsDefault: true
}
};
if (JSON.stringify(configData.database) === JSON.stringify(newConfig)) {
// Config is unchanged, skip overwrite
resolve();
return;
}
logger.info('Generating Sqlite db configuration from environment variables');
configData.database = newConfig;
}
// Write config
fs.writeFile(filename, JSON.stringify(configData, null, 2), (err) => {
if (err) {
logger.error('Could not write db config to config file: ' + filename);
reject(err);
} else {
logger.info('Wrote db configuration to config file: ' + filename);
resolve();
}
});
} else {
resolve();
}
});
}
try {
appStart();
} catch (err) {
logger.error(err.message, err);
process.exit(1);
}

View File

@@ -1,534 +0,0 @@
const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error');
const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth');
const accessListClientModel = require('../models/access_list_client');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const utils = require('../lib/utils');
function omissions () {
return ['is_deleted'];
}
const internalAccessList = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
return access.can('access_lists:create', data)
.then((/*access_data*/) => {
return accessListModel
.query()
.omit(omissions())
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1)
});
})
.then((row) => {
data.id = row.id;
let promises = [];
// Now add the items
data.items.map((item) => {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: row.id,
username: item.username,
password: item.password
})
);
});
// Now add the clients
if (typeof data.clients !== 'undefined' && data.clients) {
data.clients.map((client) => {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: row.id,
address: client.address,
directive: client.directive
})
);
});
}
return Promise.all(promises);
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.name]
* @param {String} [data.items]
* @return {Promise}
*/
update: (access, data) => {
return access.can('access_lists:update', data.id)
.then((/*access_data*/) => {
return internalAccessList.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
})
.then(() => {
// patch name if specified
if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
.query()
.where({id: data.id})
.patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
})
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
data.items.map(function (item) {
if (item.password) {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: data.id,
username: item.username,
password: item.password
})
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
});
let query = accessListAuthModel
.query()
.delete()
.where('access_list_id', data.id);
if (items_to_keep.length) {
query.andWhere('username', 'NOT IN', items_to_keep);
}
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let query = accessListClientModel
.query()
.delete()
.where('access_list_id', data.id);
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'access-list',
object_id: data.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skip_masking]
* @return {Promise}
*/
get: (access, data, skip_masking) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('access_lists:get', data.id)
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
.omit(['access_list.is_deleted'])
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('access_lists:delete', data.id)
.then(() => {
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
return accessListModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where('access_list_id', '=', row.id)
.patch({access_list_id: 0})
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0;
});
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
})
.then(() => {
return internalNginx.reload();
});
}
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
});
})
.then(() => {
return true;
});
},
/**
* All Lists
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('access_lists:list')
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.omit(['access_list.is_deleted'])
.allowEager('[owner,items,clients]')
.orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('name', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
})
.then((rows) => {
if (rows) {
rows.map(function (row, idx) {
if (typeof row.items !== 'undefined' && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
});
}
return rows;
});
},
/**
* Report use
*
* @param {Integer} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = accessListModel
.query()
.count('id as count')
.where('is_deleted', 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
},
/**
* @param {Object} list
* @returns {Object}
*/
maskItems: (list) => {
if (list && typeof list.items !== 'undefined') {
list.items.map(function (val, idx) {
let repeat_for = 8;
let first_char = '*';
if (typeof val.password !== 'undefined' && val.password) {
repeat_for = val.password.length - 1;
first_char = val.password.charAt(0);
}
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
list.items[idx].password = '';
});
}
return list;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @returns {String}
*/
getFilename: (list) => {
return '/data/access/' + list.id;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @param {String} list.name
* @param {Array} list.items
* @returns {Promise}
*/
build: (list) => {
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
return new Promise((resolve, reject) => {
let htpasswd_file = internalAccessList.getFilename(list);
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
// 2. create empty access file
try {
fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
})
.then((htpasswd_file) => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
.then((/*result*/) => {
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results);
});
});
}
});
}
};
module.exports = internalAccessList;

View File

@@ -0,0 +1,25 @@
package context
var (
// BodyCtxKey is the name of the Body value on the context
BodyCtxKey = &contextKey{"Body"}
// UserIDCtxKey is the name of the UserID value on the context
UserIDCtxKey = &contextKey{"UserID"}
// FiltersCtxKey is the name of the Filters value on the context
FiltersCtxKey = &contextKey{"Filters"}
// PrettyPrintCtxKey is the name of the pretty print context
PrettyPrintCtxKey = &contextKey{"Pretty"}
// ExpansionCtxKey is the name of the expansion context
ExpansionCtxKey = &contextKey{"Expansion"}
)
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "context value: " + k.name
}

View File

@@ -0,0 +1,208 @@
package filters
import (
"fmt"
"strings"
)
// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware
func NewFilterSchema(fieldSchemas []string) string {
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
}
// BoolFieldSchema returns the Field Schema for a Boolean accepted value field
func BoolFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, boolModifiers, filterBool, filterBool)
}
// IntFieldSchema returns the Field Schema for a Integer accepted value field
func IntFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^[0-9]+$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9]+$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// StringFieldSchema returns the Field Schema for a String accepted value field
func StringFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, stringModifiers, filterString, filterString)
}
// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
func RegexFieldSchema(fieldName string, regex string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "%s"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "%s"
}
}
]
}
}
}`, fieldName, stringModifiers, regex, regex)
}
// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
func DateFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format
// 2020-03-01T10:30:00+10:00
func DateTimeFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
const allModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
}`
const boolModifiers = `{
"type": "string",
"pattern": "^(equals|not)$"
}`
const stringModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
}`
const filterBool = `{
"type": "string",
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
}`
const filterString = `{
"type": "string",
"minLength": 1
}`
const baseFilterSchema = `{
"type": "array",
"items": {
"oneOf": [
%s
]
}
}`

View File

@@ -0,0 +1,54 @@
package handler
import (
"encoding/json"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/entity/auth"
"npm/internal/logger"
)
// SetAuth ...
// Route: POST /users/:userID/auth
func SetAuth() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
// TODO:
// delete old auth for user
// test endpoint
var newAuth auth.Model
err := json.Unmarshal(bodyBytes, &newAuth)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
userID, _, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
newAuth.UserID = userID
if newAuth.Type == auth.TypePassword {
err := newAuth.SetPassword(newAuth.Secret)
if err != nil {
logger.Error("SetPasswordError", err)
}
}
if err = newAuth.Save(); err != nil {
logger.Error("AuthSaveError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
return
}
newAuth.Secret = ""
h.ResultResponseJSON(w, r, http.StatusOK, newAuth)
}
}

View File

@@ -0,0 +1,126 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/certificateauthority"
)
// GetCertificateAuthorities will return a list of Certificate Authorities
// Route: GET /certificate-authorities
func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
}
}
}
// GetCertificateAuthority will return a single Certificate Authority
// Route: GET /certificate-authorities/{caID}
func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert)
}
}
}
// CreateCertificateAuthority will create a Certificate Authority
// Route: POST /certificate-authorities
func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newCA certificateauthority.Model
err := json.Unmarshal(bodyBytes, &newCA)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newCA.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newCA)
}
}
// UpdateCertificateAuthority ...
// Route: PUT /certificate-authorities/{caID}
func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
ca, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &ca)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = ca.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, ca)
}
}
}
// DeleteCertificateAuthority ...
// Route: DELETE /certificate-authorities/{caID}
func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
}
}
}

View File

@@ -0,0 +1,145 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/api/schema"
"npm/internal/entity/certificate"
)
// GetCertificates will return a list of Certificates
// Route: GET /certificates
func GetCertificates() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
}
}
}
// GetCertificate will return a single Certificate
// Route: GET /certificates/{certificateID}
func GetCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert)
}
}
}
// CreateCertificate will create a Certificate
// Route: POST /certificates
func CreateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newCertificate certificate.Model
err := json.Unmarshal(bodyBytes, &newCertificate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newCertificate.UserID = userID
if err = newCertificate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
}
}
// UpdateCertificate ...
// Route: PUT /certificates/{certificateID}
func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificateObject, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// This is a special endpoint, as it needs to verify the schema payload
// based on the certificate type, without being given a type in the payload.
// The middleware would normally handle this.
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes)
if jsonErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
return
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return
}
err := json.Unmarshal(bodyBytes, &certificateObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = certificateObject.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
}
}
}
// DeleteCertificate ...
// Route: DELETE /certificates/{certificateID}
func DeleteCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
}
}
}

View File

@@ -0,0 +1,15 @@
package handler
import (
"net/http"
h "npm/internal/api/http"
"npm/internal/config"
)
// Config returns the entire configuration, for debug purposes
// Route: GET /config
func Config() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration)
}
}

View File

@@ -0,0 +1,129 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/dnsprovider"
)
// GetDNSProviders will return a list of DNS Providers
// Route: GET /dns-providers
func GetDNSProviders() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, items)
}
}
}
// GetDNSProvider will return a single DNS Provider
// Route: GET /dns-providers/{providerID}
func GetDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
}
}
// CreateDNSProvider will create a DNS Provider
// Route: POST /dns-providers
func CreateDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newItem dnsprovider.Model
err := json.Unmarshal(bodyBytes, &newItem)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newItem.UserID = userID
if err = newItem.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newItem)
}
}
// UpdateDNSProvider ...
// Route: PUT /dns-providers/{providerID}
func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &item)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = item.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
}
}
// DeleteDNSProvider ...
// Route: DELETE /dns-providers/{providerID}
func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
}
}
}

View File

@@ -0,0 +1,31 @@
package handler
import (
"net/http"
h "npm/internal/api/http"
"npm/internal/config"
)
type healthCheckResponse struct {
Version string `json:"version"`
Commit string `json:"commit"`
Healthy bool `json:"healthy"`
IsSetup bool `json:"setup"`
ErrorReporting bool `json:"error_reporting"`
}
// Health returns the health of the api
// Route: GET /health
func Health() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
health := healthCheckResponse{
Version: config.Version,
Commit: config.Commit,
Healthy: true,
IsSetup: config.IsSetup,
ErrorReporting: config.ErrorReporting,
}
h.ResultResponseJSON(w, r, http.StatusOK, health)
}
}

View File

@@ -0,0 +1,151 @@
package handler
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"npm/internal/model"
"github.com/go-chi/chi"
)
const defaultLimit = 10
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
var pageInfo model.PageInfo
var err error
pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r)
if err != nil {
return pageInfo, err
}
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
if err != nil {
return pageInfo, err
}
pageInfo.Sort = getSortParameter(r)
return pageInfo, nil
}
func getDateRanges(r *http.Request) (time.Time, time.Time, error) {
queryValues := r.URL.Query()
from := queryValues.Get("from")
fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default
to := queryValues.Get("to")
toDate := time.Now()
if from != "" {
var fromErr error
fromDate, fromErr = time.Parse(time.RFC3339, from)
if fromErr != nil {
return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
}
}
if to != "" {
var toErr error
toDate, toErr = time.Parse(time.RFC3339, to)
if toErr != nil {
return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
}
}
return fromDate, toDate, nil
}
func getSortParameter(r *http.Request) []model.Sort {
var sortFields []model.Sort
queryValues := r.URL.Query()
sortString := queryValues.Get("sort")
if sortString == "" {
return sortFields
}
// Split sort fields up in to slice
sorts := strings.Split(sortString, ",")
for _, sortItem := range sorts {
if strings.Contains(sortItem, ".") {
theseItems := strings.Split(sortItem, ".")
switch strings.ToLower(theseItems[1]) {
case "desc":
fallthrough
case "descending":
theseItems[1] = "DESC"
default:
theseItems[1] = "ASC"
}
sortFields = append(sortFields, model.Sort{
Field: theseItems[0],
Direction: theseItems[1],
})
} else {
sortFields = append(sortFields, model.Sort{
Field: sortItem,
Direction: "ASC",
})
}
}
return sortFields
}
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
queryValues := r.URL.Query()
varValue := queryValues.Get(varName)
if varValue == "" && required {
return 0, fmt.Errorf("%v was not supplied in the request", varName)
} else if varValue == "" {
return defaultValue, nil
}
varInt, intErr := strconv.Atoi(varValue)
if intErr != nil {
return 0, fmt.Errorf("%v is not a valid number", varName)
}
return varInt, nil
}
func getURLParamInt(r *http.Request, varName string) (int, error) {
required := true
defaultValue := 0
paramStr := chi.URLParam(r, varName)
var err error
var paramInt int
if paramStr == "" && required {
return 0, fmt.Errorf("%v was not supplied in the request", varName)
} else if paramStr == "" {
return defaultValue, nil
}
if paramInt, err = strconv.Atoi(paramStr); err != nil {
return 0, fmt.Errorf("%v is not a valid number", varName)
}
return paramInt, nil
}
func getPagination(r *http.Request) (int, int, error) {
var err error
offset, err := getQueryVarInt(r, "offset", false, 0)
if err != nil {
return 0, 0, err
}
limit, err := getQueryVarInt(r, "limit", false, defaultLimit)
if err != nil {
return 0, 0, err
}
return offset, limit, nil
}

View File

@@ -0,0 +1,135 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/host"
"npm/internal/validator"
)
// GetHosts will return a list of Hosts
// Route: GET /hosts
func GetHosts() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
}
// GetHost will return a single Host
// Route: GET /hosts/{hostID}
func GetHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateHost will create a Host
// Route: POST /hosts
func CreateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHost host.Model
err := json.Unmarshal(bodyBytes, &newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHost.UserID = userID
if err = validator.ValidateHost(newHost); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = newHost.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// UpdateHost ...
// Route: PUT /hosts/{hostID}
func UpdateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &host)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = host.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// DeleteHost ...
// Route: DELETE /hosts/{hostID}
func DeleteHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
}
}
}

View File

@@ -0,0 +1,14 @@
package handler
import (
"net/http"
h "npm/internal/api/http"
)
// NotAllowed is a json error handler for when method is not allowed
func NotAllowed() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil)
}
}

View File

@@ -0,0 +1,65 @@
package handler
import (
"embed"
"errors"
"io"
"io/fs"
"mime"
"net/http"
"path/filepath"
"strings"
h "npm/internal/api/http"
)
//go:embed assets
var assets embed.FS
var assetsSub fs.FS
var errIsDir = errors.New("path is dir")
// NotFound is a json error handler for 404's and method not allowed.
// It also serves the react frontend as embedded files in the golang binary.
func NotFound() func(http.ResponseWriter, *http.Request) {
assetsSub, _ = fs.Sub(assets, "assets")
return func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimLeft(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
err := tryRead(assetsSub, path, w)
if err == errIsDir {
err = tryRead(assetsSub, "index.html", w)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
}
} else if err == nil {
return
}
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
}
}
func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error {
f, err := folder.Open(requestedPath)
if err != nil {
return err
}
// nolint: errcheck
defer f.Close()
stat, _ := f.Stat()
if stat.IsDir() {
return errIsDir
}
contentType := mime.TypeByExtension(filepath.Ext(requestedPath))
w.Header().Set("Content-Type", contentType)
_, err = io.Copy(w, f)
return err
}

View File

@@ -0,0 +1,99 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"npm/doc"
"npm/internal/api/schema"
"npm/internal/config"
"npm/internal/logger"
jsref "github.com/jc21/jsref"
"github.com/jc21/jsref/provider"
)
var swaggerSchema []byte
// Schema simply reads the swagger schema from disk and returns is raw
// Route: GET /schema
func Schema() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(getSchema()))
}
}
func getSchema() []byte {
if swaggerSchema == nil {
// nolint:gosec
swaggerSchema, _ = doc.SwaggerFiles.ReadFile("api.swagger.json")
// Replace {{VERSION}} with Config Version
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
// Dereference the JSON Schema:
var schema interface{}
if err := json.Unmarshal(swaggerSchema, &schema); err != nil {
logger.Error("SwaggerUnmarshalError", err)
return nil
}
provider := provider.NewIoFS(doc.SwaggerFiles, "")
resolver := jsref.New()
err := resolver.AddProvider(provider)
if err != nil {
logger.Error("SchemaProviderError", err)
}
result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
if err != nil {
logger.Error("SwaggerResolveError", err)
} else {
var marshalErr error
swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ")
if marshalErr != nil {
logger.Error("SwaggerMarshalError", err)
}
}
// End dereference
// Replace incoming schemas with those we actually use in code
swaggerSchema = replaceIncomingSchemas(swaggerSchema)
}
return swaggerSchema
}
func replaceIncomingSchemas(swaggerSchema []byte) []byte {
str := string(swaggerSchema)
// Remember to include the double quotes in the replacement!
str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth())
str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken())
str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority())
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority())
str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate())
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate(""))
str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting())
str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting())
str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser())
str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser())
str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost())
str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost())
str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream())
str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream())
str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider())
str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider())
return []byte(str)
}

View File

@@ -0,0 +1,98 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/setting"
"github.com/go-chi/chi"
)
// GetSettings will return a list of Settings
// Route: GET /settings
func GetSettings() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, settings)
}
}
}
// GetSetting will return a single Setting
// Route: GET /settings/{name}
func GetSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
sett, err := setting.GetByName(name)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, sett)
}
}
}
// CreateSetting will create a Setting
// Route: POST /settings
func CreateSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newSetting setting.Model
err := json.Unmarshal(bodyBytes, &newSetting)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newSetting.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newSetting)
}
}
// UpdateSetting ...
// Route: PUT /settings/{name}
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
settingName := chi.URLParam(r, "name")
setting, err := setting.GetByName(settingName)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &setting)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = setting.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, setting)
}
}
}

View File

@@ -0,0 +1,129 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/stream"
)
// GetStreams will return a list of Streams
// Route: GET /hosts/streams
func GetStreams() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
}
// GetStream will return a single Streams
// Route: GET /hosts/streams/{hostID}
func GetStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateStream will create a Stream
// Route: POST /hosts/steams
func CreateStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHost stream.Model
err := json.Unmarshal(bodyBytes, &newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHost.UserID = userID
if err = newHost.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// UpdateStream ...
// Route: PUT /hosts/streams/{hostID}
func UpdateStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &host)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = host.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// DeleteStream ...
// Route: DELETE /hosts/streams/{hostID}
func DeleteStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
}
}
}

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