Compare commits

...

18 Commits

Author SHA1 Message Date
f3e6f64c0c Version Bump 2019-03-05 08:25:12 +10:00
d04b7a0d88 Bug fixes 2019-03-05 08:25:12 +10:00
71dfd5d8f8 Feature/custom locations (#74)
* New feature: custom locations

* Custom locations: exteding config generator

* Custom locations: refactoring

* Fixing proxy_host table on small screens

* Custom locations: translations

* Custom locations bugfix

* Custom locations bugfix

* PR #74 fixes
2019-03-05 08:21:02 +10:00
133d66c2fe Default Site customisation and new Settings space (#91) 2019-03-04 21:19:36 +10:00
6f1d38a0e2 Fixes #88 - Allow specifying X-FRAME-OPTIONS with an environment variable (#89) 2019-03-04 10:16:46 +10:00
aad9ecde6b CI: Prevent having to spin up resources when not Master branch 2019-03-01 20:12:49 +10:00
ae9324295c Merge branch 'develop' of github.com:jc21/nginx-proxy-manager into develop 2019-03-01 13:48:30 +10:00
0acec1105b CI: Prevent having to spin up resources when not Master branch 2019-03-01 20:12:49 +10:00
5a9a716ca6 CI: Prevent having to spin up resources when not Master branch 2019-03-01 13:47:49 +10:00
418899d425 Version bump 2019-02-27 17:52:30 +10:00
e7379e3683 Ignore default location when defined in advanced config (#79) 2019-02-25 10:42:16 +10:00
29bebcc73e Ignore default location when defined in advanced config (#79) 2019-02-25 10:34:55 +10:00
26064b20b8 Fix PR docker image pushing to wrong repo 2019-02-20 08:25:12 +10:00
3dc9b20543 CI Improvements (#77) 2019-02-20 14:35:10 +10:00
444dbd5160 Added PR build steps to CI 2019-02-20 08:25:12 +10:00
c2f99e253c Merge branch 'master' of github.com:jc21/nginx-proxy-manager 2019-02-20 10:04:16 +10:00
5c7fb7b698 Added armv6 Dockerfile 2019-02-20 08:25:12 +10:00
733d7d9583 Update DOCKERHUB.md 2019-02-19 17:05:26 +10:00
51 changed files with 1441 additions and 138 deletions

38
Dockerfile.armv6 Normal file
View File

@ -0,0 +1,38 @@
FROM jc21/nginx-proxy-manager-base:armv6
MAINTAINER Jamie Curnow <jc@jc21.com>
LABEL maintainer="Jamie Curnow <jc@jc21.com>"
ENV SUPPRESS_NO_CONFIG_WARNING=1
ENV S6_FIX_ATTRS_HIDDEN=1
RUN echo "fs.file-max = 65535" > /etc/sysctl.conf
# Nginx, Node and required packages should already be installed from the base image
# root filesystem
COPY rootfs /
# s6 overlay
RUN curl -L -o /tmp/s6-overlay-arm.tar.gz "https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-arm.tar.gz" \
&& tar xzf /tmp/s6-overlay-arm.tar.gz -C /
# App
ENV NODE_ENV=production
ADD dist /app/dist
ADD node_modules /app/node_modules
ADD src/backend /app/src/backend
ADD package.json /app/package.json
ADD knexfile.js /app/knexfile.js
# Volumes
VOLUME [ "/data", "/etc/letsencrypt" ]
CMD [ "/init" ]
# Ports
EXPOSE 80
EXPOSE 81
EXPOSE 443
EXPOSE 9876
HEALTHCHECK --interval=15s --timeout=3s CMD curl -f http://localhost:9876/health || exit 1

185
Jenkinsfile vendored
View File

@ -12,11 +12,38 @@ pipeline {
TEMP_IMAGE_ARM64 = "nginx-proxy-manager-arm64-build_${BUILD_NUMBER}" TEMP_IMAGE_ARM64 = "nginx-proxy-manager-arm64-build_${BUILD_NUMBER}"
TAG_VERSION = getPackageVersion() TAG_VERSION = getPackageVersion()
MAJOR_VERSION = "2" MAJOR_VERSION = "2"
BRANCH_LOWER = "${BRANCH_NAME.toLowerCase()}"
} }
stages { stages {
stage('Prepare') { stage('Build PR') {
when {
changeRequest()
}
steps { steps {
sh 'docker pull $DOCKER_CI_TOOLS' ansiColor('xterm') {
// Codebase
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build'
sh 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod'
sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune'
// Docker Build
sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .'
// Dockerhub
sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/${IMAGE}:github-${BRANCH_LOWER}'
}
sh 'docker rmi ${TEMP_IMAGE}'
script {
def comment = pullRequest.comment("Docker Image for build ${BUILD_NUMBER} is available on [DockerHub](https://cloud.docker.com/repository/docker/jc21/${IMAGE}) as `jc21/${IMAGE}:github-${BRANCH_LOWER}`")
}
}
} }
} }
stage('Build Develop') { stage('Build Develop') {
@ -26,168 +53,162 @@ pipeline {
steps { steps {
ansiColor('xterm') { ansiColor('xterm') {
// Codebase // Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE yarn install' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE npm run-script build' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build'
sh 'rm -rf node_modules' sh 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE yarn install --prod' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod'
sh 'docker run --rm -v $(pwd):/data $DOCKER_CI_TOOLS node-prune' sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune'
// Docker Build // Docker Build
sh 'docker build --pull --no-cache --squash --compress -t $TEMP_IMAGE .' sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .'
// Dockerhub // Dockerhub
sh 'docker tag $TEMP_IMAGE docker.io/jc21/$IMAGE:develop' sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:develop'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'" sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/$IMAGE:develop' sh 'docker push docker.io/jc21/${IMAGE}:develop'
} }
// Private Registry // Private Registry
sh 'docker tag $TEMP_IMAGE $DOCKER_PRIVATE_REGISTRY/$IMAGE:develop' sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:develop'
withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass' $DOCKER_PRIVATE_REGISTRY" sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}"
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:develop' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:develop'
} }
sh 'docker rmi $TEMP_IMAGE' sh 'docker rmi ${TEMP_IMAGE}'
} }
} }
} }
stage('Build Master') { stage('Build Master') {
when {
branch 'master'
}
parallel { parallel {
stage('x86_64') { stage('x86_64') {
when {
branch 'master'
}
steps { steps {
ansiColor('xterm') { ansiColor('xterm') {
// Codebase // Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE yarn install' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE npm run-script build' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} npm run-script build'
sh 'rm -rf node_modules' sh 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE yarn install --prod' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE} yarn install --prod'
sh 'docker run --rm -v $(pwd):/data $DOCKER_CI_TOOLS node-prune' sh 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} node-prune'
// Docker Build // Docker Build
sh 'docker build --pull --no-cache --squash --compress -t $TEMP_IMAGE .' sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE} .'
// Dockerhub // Dockerhub
sh 'docker tag $TEMP_IMAGE docker.io/jc21/$IMAGE:$TAG_VERSION' sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:${TAG_VERSION}'
sh 'docker tag $TEMP_IMAGE docker.io/jc21/$IMAGE:$MAJOR_VERSION' sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}'
sh 'docker tag $TEMP_IMAGE docker.io/jc21/$IMAGE:latest' sh 'docker tag ${TEMP_IMAGE} docker.io/jc21/${IMAGE}:latest'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'" sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/$IMAGE:$TAG_VERSION' sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}'
sh 'docker push docker.io/jc21/$IMAGE:$MAJOR_VERSION' sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}'
sh 'docker push docker.io/jc21/$IMAGE:latest' sh 'docker push docker.io/jc21/${IMAGE}:latest'
} }
// Private Registry // Private Registry
sh 'docker tag $TEMP_IMAGE $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION' sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}'
sh 'docker tag $TEMP_IMAGE $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION' sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}'
sh 'docker tag $TEMP_IMAGE $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest' sh 'docker tag ${TEMP_IMAGE} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest'
withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass' $DOCKER_PRIVATE_REGISTRY" sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}"
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest'
} }
sh 'docker rmi $TEMP_IMAGE' sh 'docker rmi ${TEMP_IMAGE}'
} }
} }
} }
stage('armhf') { stage('armhf') {
when {
branch 'master'
}
agent { agent {
label 'armhf' label 'armhf'
} }
steps { steps {
ansiColor('xterm') { ansiColor('xterm') {
// Codebase // Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:armhf yarn install' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:armhf npm run-script build' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf npm run-script build'
sh 'rm -rf node_modules' sh 'rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:armhf yarn install --prod' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:armhf yarn install --prod'
// Docker Build // Docker Build
sh 'docker build --pull --no-cache --squash --compress -t $TEMP_IMAGE_ARM -f Dockerfile.armhf .' sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM} -f Dockerfile.armhf .'
// Dockerhub // Dockerhub
sh 'docker tag $TEMP_IMAGE_ARM docker.io/jc21/$IMAGE:$TAG_VERSION-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:${TAG_VERSION}-armhf'
sh 'docker tag $TEMP_IMAGE_ARM docker.io/jc21/$IMAGE:$MAJOR_VERSION-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-armhf'
sh 'docker tag $TEMP_IMAGE_ARM docker.io/jc21/$IMAGE:latest-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} docker.io/jc21/${IMAGE}:latest-armhf'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'" sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/$IMAGE:$TAG_VERSION-armhf' sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-armhf'
sh 'docker push docker.io/jc21/$IMAGE:$MAJOR_VERSION-armhf' sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-armhf'
sh 'docker push docker.io/jc21/$IMAGE:latest-armhf' sh 'docker push docker.io/jc21/${IMAGE}:latest-armhf'
} }
// Private Registry // Private Registry
sh 'docker tag $TEMP_IMAGE_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-armhf'
sh 'docker tag $TEMP_IMAGE_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-armhf'
sh 'docker tag $TEMP_IMAGE_ARM $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest-armhf' sh 'docker tag ${TEMP_IMAGE_ARM} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-armhf'
withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass' $DOCKER_PRIVATE_REGISTRY" sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}"
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION-armhf' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-armhf'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION-armhf' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-armhf'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest-armhf' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-armhf'
} }
sh 'docker rmi $TEMP_IMAGE_ARM' sh 'docker rmi ${TEMP_IMAGE_ARM}'
} }
} }
} }
stage('arm64') { stage('arm64') {
when {
branch 'master'
}
agent { agent {
label 'arm64' label 'arm64'
} }
steps { steps {
ansiColor('xterm') { ansiColor('xterm') {
// Codebase // Codebase
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:arm64 yarn install' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 yarn install'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:arm64 npm run-script build' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 npm run-script build'
sh 'sudo rm -rf node_modules' sh 'sudo rm -rf node_modules'
sh 'docker run --rm -v $(pwd):/app -w /app $BASE_IMAGE:arm64 yarn install --prod' sh 'docker run --rm -v $(pwd):/app -w /app ${BASE_IMAGE}:arm64 yarn install --prod'
// Docker Build // Docker Build
sh 'docker build --pull --no-cache --squash --compress -t $TEMP_IMAGE_ARM64 -f Dockerfile.arm64 .' sh 'docker build --pull --no-cache --squash --compress -t ${TEMP_IMAGE_ARM64} -f Dockerfile.arm64 .'
// Dockerhub // Dockerhub
sh 'docker tag $TEMP_IMAGE_ARM64 docker.io/jc21/$IMAGE:$TAG_VERSION-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:${TAG_VERSION}-arm64'
sh 'docker tag $TEMP_IMAGE_ARM64 docker.io/jc21/$IMAGE:$MAJOR_VERSION-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-arm64'
sh 'docker tag $TEMP_IMAGE_ARM64 docker.io/jc21/$IMAGE:latest-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} docker.io/jc21/${IMAGE}:latest-arm64'
withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-dockerhub', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass'" sh "docker login -u '${duser}' -p '${dpass}'"
sh 'docker push docker.io/jc21/$IMAGE:$TAG_VERSION-arm64' sh 'docker push docker.io/jc21/${IMAGE}:${TAG_VERSION}-arm64'
sh 'docker push docker.io/jc21/$IMAGE:$MAJOR_VERSION-arm64' sh 'docker push docker.io/jc21/${IMAGE}:${MAJOR_VERSION}-arm64'
sh 'docker push docker.io/jc21/$IMAGE:latest-arm64' sh 'docker push docker.io/jc21/${IMAGE}:latest-arm64'
} }
// Private Registry // Private Registry
sh 'docker tag $TEMP_IMAGE_ARM64 $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-arm64'
sh 'docker tag $TEMP_IMAGE_ARM64 $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-arm64'
sh 'docker tag $TEMP_IMAGE_ARM64 $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest-arm64' sh 'docker tag ${TEMP_IMAGE_ARM64} ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-arm64'
withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) { withCredentials([usernamePassword(credentialsId: 'jc21-private-registry', passwordVariable: 'dpass', usernameVariable: 'duser')]) {
sh "docker login -u '${duser}' -p '$dpass' $DOCKER_PRIVATE_REGISTRY" sh "docker login -u '${duser}' -p '${dpass}' ${DOCKER_PRIVATE_REGISTRY}"
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$TAG_VERSION-arm64' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${TAG_VERSION}-arm64'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:$MAJOR_VERSION-arm64' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:${MAJOR_VERSION}-arm64'
sh 'docker push $DOCKER_PRIVATE_REGISTRY/$IMAGE:latest-arm64' sh 'docker push ${DOCKER_PRIVATE_REGISTRY}/${IMAGE}:latest-arm64'
} }
sh 'docker rmi $TEMP_IMAGE_ARM64' sh 'docker rmi ${TEMP_IMAGE_ARM64}'
// Hack to clean up ec2 instance for next build // Hack to clean up ec2 instance for next build
sh 'sudo chown -R ec2-user:ec2-user *' sh 'sudo chown -R ec2-user:ec2-user *'
@ -210,7 +231,7 @@ pipeline {
} }
def getPackageVersion() { def getPackageVersion() {
ver = sh(script: 'docker run --rm -v $(pwd):/data $DOCKER_CI_TOOLS bash -c "cat /data/package.json|jq -r \'.version\'"', returnStdout: true) ver = sh(script: 'docker run --rm -v $(pwd):/data ${DOCKER_CI_TOOLS} bash -c "cat /data/package.json|jq -r \'.version\'"', returnStdout: true)
return ver.trim() return ver.trim()
} }

View File

@ -2,7 +2,7 @@
# Nginx Proxy Manager # Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-2.0.9-green.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/version-2.0.11-green.svg?style=for-the-badge)
![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge)
![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge)
@ -57,24 +57,6 @@ Please consult the [installation instructions](doc/INSTALL.md) for a complete gu
if you just want to get up and running in the quickest time possible, grab all the files in the `doc/example/` folder and run `docker-compose up -d` if you just want to get up and running in the quickest time possible, grab all the files in the `doc/example/` folder and run `docker-compose up -d`
## Importing from Version 1?
Here's a [guide for you to migrate your configuration](doc/IMPORTING.md). You should definitely read the [installation instructions](doc/INSTALL.md) first though.
**Why should I?**
Version 2 has the following improvements:
- Management security and multiple user access
- User permissions and visibility
- Custom SSL certificate support
- Audit log of changes
- Broken nginx config detection
- Multiple domains in Let's Encrypt certificates
- Wildcard domain name support (not available with a Let's Encrypt certificate though)
- It's super sexy
## Administration ## Administration
When your docker container is running, connect to it on port `81` for the admin interface. When your docker container is running, connect to it on port `81` for the admin interface.

View File

@ -2,7 +2,7 @@
# Nginx Proxy Manager # Nginx Proxy Manager
![Version](https://img.shields.io/badge/version-2.0.9-green.svg?style=for-the-badge) ![Version](https://img.shields.io/badge/version-2.0.11-green.svg?style=for-the-badge)
![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Stars](https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge)
![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge) ![Pulls](https://img.shields.io/docker/pulls/jc21/nginx-proxy-manager.svg?style=for-the-badge)
@ -16,6 +16,7 @@ running at home or otherwise, including free SSL, without having to know too muc
* latest 2, 2.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile)) * latest 2, 2.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile))
* latest-armhf, 2-armhf, 2.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.armhf)) * latest-armhf, 2-armhf, 2.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.armhf))
* latest-arm64, 2-arm64, 2.x.x-arm64 ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/master/Dockerfile.arm64))
* 1, 1.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile)) * 1, 1.x.x ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile))
* 1-armhf, 1.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile.armhf)) * 1-armhf, 1.x.x-armhf ([Dockerfile](https://github.com/jc21/nginx-proxy-manager/blob/1.1.2/Dockerfile.armhf))

View File

@ -143,3 +143,23 @@ Password: changeme
``` ```
Immediately after logging in with this default user you will be asked to modify your details and change your password. Immediately after logging in with this default user you will be asked to modify your details and change your password.
### Advanced Options
#### X-FRAME-OPTIONS Header
You can configure the [`X-FRAME-OPTIONS`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) header
value by specifying it as a Docker environment variable. The default if not specified is `deny`.
```yml
...
environment:
X_FRAME_OPTIONS: "sameorigin"
...
```
```
... -e "X_FRAME_OPTIONS=sameorigin" ...
```

View File

@ -1,6 +1,6 @@
{ {
"name": "nginx-proxy-manager", "name": "nginx-proxy-manager",
"version": "2.0.9", "version": "2.0.11",
"description": "A beautiful interface for creating Nginx endpoints", "description": "A beautiful interface for creating Nginx endpoints",
"main": "src/backend/index.js", "main": "src/backend/index.js",
"devDependencies": { "devDependencies": {

View File

@ -22,10 +22,10 @@ server {
} }
} }
# Default 80 Host, which shows a "You are not configured" page # "You are not configured" page, which is the default if another default doesn't exist
server { server {
listen 80 default; listen 80;
server_name localhost; server_name localhost-nginx-proxy-manager;
access_log /data/logs/default.log proxy; access_log /data/logs/default.log proxy;
@ -38,9 +38,9 @@ server {
} }
} }
# Default 443 Host # First 443 Host, which is the default if another default doesn't exist
server { server {
listen 443 ssl default; listen 443 ssl;
server_name localhost; server_name localhost;
access_log /data/logs/default.log proxy; access_log /data/logs/default.log proxy;

View File

@ -70,6 +70,7 @@ http {
# Files generated by NPM # Files generated by NPM
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
include /data/nginx/default_host/*.conf;
include /data/nginx/proxy_host/*.conf; include /data/nginx/proxy_host/*.conf;
include /data/nginx/redirection_host/*.conf; include /data/nginx/redirection_host/*.conf;
include /data/nginx/dead_host/*.conf; include /data/nginx/dead_host/*.conf;

View File

@ -7,6 +7,8 @@ mkdir -p /tmp/nginx/body \
/data/custom_ssl \ /data/custom_ssl \
/data/logs \ /data/logs \
/data/access \ /data/access \
/data/nginx/default_host \
/data/nginx/default_www \
/data/nginx/proxy_host \ /data/nginx/proxy_host \
/data/nginx/redirection_host \ /data/nginx/redirection_host \
/data/nginx/stream \ /data/nginx/stream \

View File

@ -40,11 +40,17 @@ app.use(require('./lib/express/cors'));
// General security/cache related headers + server header // General security/cache related headers + server header
app.use(function (req, res, next) { 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({ res.set({
'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload', 'Strict-Transport-Security': 'includeSubDomains; max-age=631138519; preload',
'X-XSS-Protection': '0', 'X-XSS-Protection': '0',
'X-Content-Type-Options': 'nosniff', 'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY', 'X-Frame-Options': x_frame_options,
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
Pragma: 'no-cache', Pragma: 'no-cache',
Expires: 0 Expires: 0

View File

@ -1,7 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
const logger = require('./logger').global; const logger = require('./logger').global;
function appStart () { function appStart () {

View File

@ -1,5 +1,3 @@
'use strict';
const _ = require('lodash'); const _ = require('lodash');
const fs = require('fs'); const fs = require('fs');
const Liquid = require('liquidjs'); const Liquid = require('liquidjs');
@ -19,9 +17,9 @@ const internalNginx = {
* - IF BAD: update the meta with offline status and remove the config entirely * - IF BAD: update the meta with offline status and remove the config entirely
* - then reload nginx * - then reload nginx
* *
* @param {Object} model * @param {Object|String} model
* @param {String} host_type * @param {String} host_type
* @param {Object} host * @param {Object} host
* @returns {Promise} * @returns {Promise}
*/ */
configure: (model, host_type, host) => { configure: (model, host_type, host) => {
@ -92,7 +90,7 @@ const internalNginx = {
}) })
.then(() => { .then(() => {
return combined_meta; return combined_meta;
}) });
}, },
/** /**
@ -124,9 +122,43 @@ const internalNginx = {
*/ */
getConfigName: (host_type, host_id) => { getConfigName: (host_type, host_id) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_'); host_type = host_type.replace(new RegExp('-', 'g'), '_');
if (host_type === 'default') {
return '/data/nginx/default_host/site.conf';
}
return '/data/nginx/' + host_type + '/' + host_id + '.conf'; return '/data/nginx/' + host_type + '/' + host_id + '.conf';
}, },
/**
* Generates custom locations
* @param {Object} host
* @returns {Promise}
*/
renderLocations: (host) => {
return new Promise((resolve, reject) => {
let template;
try {
template = fs.readFileSync(__dirname + '/../templates/_location.conf', {encoding: 'utf8'});
} catch (err) {
reject(new error.ConfigurationError(err.message));
return;
}
let renderer = new Liquid();
let renderedLocations = '';
const locationRendering = async () => {
for (let i = 0; i < host.locations.length; i++) {
renderedLocations += await renderer.parseAndRender(template, host.locations[i]);
}
}
locationRendering().then(() => resolve(renderedLocations));
});
},
/** /**
* @param {String} host_type * @param {String} host_type
* @param {Object} host * @param {Object} host
@ -146,6 +178,7 @@ const internalNginx = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let template = null; let template = null;
let filename = internalNginx.getConfigName(host_type, host.id); let filename = internalNginx.getConfigName(host_type, host.id);
try { try {
template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'}); template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
} catch (err) { } catch (err) {
@ -153,24 +186,49 @@ const internalNginx = {
return; return;
} }
renderEngine let locationsPromise;
.parseAndRender(template, host) let origLocations;
.then(config_text => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (debug_mode) { // Manipulate the data a bit before sending it to the template
logger.success('Wrote config:', filename, config_text); if (host_type !== 'default') {
} host.use_default_location = true;
if (typeof host.advanced_config !== 'undefined' && host.advanced_config) {
host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config);
}
}
resolve(true); if (host.locations) {
}) origLocations = [].concat(host.locations);
.catch(err => { locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => {
if (debug_mode) { host.locations = renderedLocations;
logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
}); });
} else {
locationsPromise = Promise.resolve();
}
locationsPromise.then(() => {
renderEngine
.parseAndRender(template, host)
.then(config_text => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
if (debug_mode) {
logger.success('Wrote config:', filename, config_text);
}
// Restore locations array
host.locations = origLocations;
resolve(true);
})
.catch(err => {
if (debug_mode) {
logger.warn('Could not write ' + filename + ':', err.message);
}
reject(new error.ConfigurationError(err.message));
});
});
}); });
}, },
@ -255,7 +313,7 @@ const internalNginx = {
/** /**
* @param {String} host_type * @param {String} host_type
* @param {Object} host * @param {Object} [host]
* @param {Boolean} [throw_errors] * @param {Boolean} [throw_errors]
* @returns {Promise} * @returns {Promise}
*/ */
@ -264,7 +322,7 @@ const internalNginx = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
let config_file = internalNginx.getConfigName(host_type, host.id); let config_file = internalNginx.getConfigName(host_type, typeof host === 'undefined' ? 0 : host.id);
if (debug_mode) { if (debug_mode) {
logger.warn('Deleting nginx config: ' + config_file); logger.warn('Deleting nginx config: ' + config_file);
@ -312,6 +370,14 @@ const internalNginx = {
}); });
return Promise.all(promises); return Promise.all(promises);
},
/**
* @param {string} config
* @returns {boolean}
*/
advancedConfigHasDefaultLocation: function (config) {
return !!config.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im);
} }
}; };

View File

@ -108,7 +108,7 @@ const internalProxyHost = {
*/ */
update: (access, data) => { update: (access, data) => {
let create_certificate = data.certificate_id === 'new'; let create_certificate = data.certificate_id === 'new';
console.log('PH UPDATE:', data);
if (create_certificate) { if (create_certificate) {
delete data.certificate_id; delete data.certificate_id;
} }

View File

@ -0,0 +1,133 @@
const fs = require('fs');
const error = require('../lib/error');
const settingModel = require('../models/setting');
const internalNginx = require('./nginx');
const internalSetting = {
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
update: (access, data) => {
return access.can('settings:update', data.id)
.then(access_data => {
return internalSetting.get(access, {id: data.id});
})
.then(row => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Setting could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
return settingModel
.query()
.where({id: data.id})
.patch(data);
})
.then(() => {
return internalSetting.get(access, {
id: data.id
});
})
.then(row => {
if (row.id === 'default-site') {
// write the html if we need to
if (row.value === 'html') {
fs.writeFileSync('/data/nginx/default_www/index.html', row.meta.html, {encoding: 'utf8'});
}
// Configure nginx
return internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.generateConfig('default', row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
return row;
})
.catch((err) => {
internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
})
.then(() => {
// I'm being slack here I know..
throw new error.ValidationError('Could not reconfigure Nginx. Please check logs.');
})
});
} else {
return row;
}
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {String} data.id
* @return {Promise}
*/
get: (access, data) => {
return access.can('settings:get', data.id)
.then(() => {
return settingModel
.query()
.where('id', data.id)
.first();
})
.then(row => {
if (row) {
return row;
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
* This will only count the settings
*
* @param {Access} access
* @returns {*}
*/
getCount: (access) => {
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.count('id as count')
.first();
})
.then(row => {
return parseInt(row.count, 10);
});
},
/**
* All settings
*
* @param {Access} access
* @returns {Promise}
*/
getAll: (access) => {
return access.can('settings:list')
.then(() => {
return settingModel
.query()
.orderBy('description', 'ASC');
});
}
};
module.exports = internalSetting;

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}

View File

@ -0,0 +1,37 @@
'use strict';
const migrate_name = 'custom_locations';
const logger = require('../logger').migrate;
/**
* Migrate
* Extends proxy_host table with locations field
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.table('proxy_host', function (proxy_host) {
proxy_host.json('locations');
})
.then(() => {
logger.info('[' + migrate_name + '] proxy_host Table altered');
})
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down this one.');
return Promise.resolve(true);
};

View File

@ -0,0 +1,54 @@
const migrate_name = 'settings';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.createTable('setting', table => {
table.string('id').notNull().primary();
table.string('name', 100).notNull();
table.string('description', 255).notNull();
table.string('value', 255).notNull();
table.json('meta').notNull();
})
.then(() => {
logger.info('[' + migrate_name + '] setting Table created');
// TODO: add settings
let settingModel = require('../models/setting');
return settingModel
.query()
.insert({
id: 'default-site',
name: 'Default Site',
description: 'What to show when Nginx is hit with an unknown Host',
value: 'congratulations',
meta: {}
});
})
.then(() => {
logger.info('[' + migrate_name + '] Default settings added');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex, Promise) {
logger.warn('[' + migrate_name + '] You can\'t migrate down the initial data.');
return Promise.resolve(true);
};

View File

@ -47,7 +47,7 @@ class ProxyHost extends Model {
} }
static get jsonAttributes () { static get jsonAttributes () {
return ['domain_names', 'meta']; return ['domain_names', 'meta', 'locations'];
} }
static get relationMappings () { static get relationMappings () {

View File

@ -0,0 +1,30 @@
// Objection Docs:
// http://vincit.github.io/objection.js/
const db = require('../db');
const Model = require('objection').Model;
Model.knex(db);
class Setting extends Model {
$beforeInsert () {
// Default for meta
if (typeof this.meta === 'undefined') {
this.meta = {};
}
}
static get name () {
return 'Setting';
}
static get tableName () {
return 'setting';
}
static get jsonAttributes () {
return ['meta'];
}
}
module.exports = Setting;

View File

@ -31,6 +31,7 @@ router.use('/tokens', require('./tokens'));
router.use('/users', require('./users')); router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log')); router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports')); router.use('/reports', require('./reports'));
router.use('/settings', require('./settings'));
router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts')); router.use('/nginx/proxy-hosts', require('./nginx/proxy_hosts'));
router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts')); router.use('/nginx/redirection-hosts', require('./nginx/redirection_hosts'));
router.use('/nginx/dead-hosts', require('./nginx/dead_hosts')); router.use('/nginx/dead-hosts', require('./nginx/dead_hosts'));

View File

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

View File

@ -9,6 +9,13 @@
"type": "integer", "type": "integer",
"minimum": 1 "minimum": 1
}, },
"setting_id": {
"description": "Unique identifier for a Setting",
"example": "default-site",
"readOnly": true,
"type": "string",
"minLength": 2
},
"token": { "token": {
"type": "string", "type": "string",
"minLength": 10 "minLength": 10

View File

@ -69,6 +69,41 @@
}, },
"meta": { "meta": {
"type": "object" "type": "object"
},
"locations": {
"type": "array",
"minItems": 0,
"items": {
"type": "object",
"required": [
"forward_scheme",
"forward_host",
"forward_port",
"path"
],
"additionalProperties": false,
"properties": {
"id": {
"type": ["integer", "null"]
},
"path": {
"type": "string",
"minLength": 1
},
"forward_scheme": {
"$ref": "#/definitions/forward_scheme"
},
"forward_host": {
"$ref": "#/definitions/forward_host"
},
"forward_port": {
"$ref": "#/definitions/forward_port"
},
"advanced_config": {
"type": "string"
}
}
}
} }
}, },
"properties": { "properties": {
@ -128,6 +163,9 @@
}, },
"meta": { "meta": {
"$ref": "#/definitions/meta" "$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
} }
}, },
"links": [ "links": [
@ -215,6 +253,9 @@
}, },
"meta": { "meta": {
"$ref": "#/definitions/meta" "$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
} }
} }
}, },
@ -285,6 +326,9 @@
}, },
"meta": { "meta": {
"$ref": "#/definitions/meta" "$ref": "#/definitions/meta"
},
"locations": {
"$ref": "#/definitions/locations"
} }
} }
}, },

View File

@ -0,0 +1,99 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "endpoints/settings",
"title": "Settings",
"description": "Endpoints relating to Settings",
"stability": "stable",
"type": "object",
"definitions": {
"id": {
"$ref": "../definitions.json#/definitions/setting_id"
},
"name": {
"description": "Name",
"example": "Default Site",
"type": "string",
"minLength": 2,
"maxLength": 100
},
"description": {
"description": "Description",
"example": "Default Site",
"type": "string",
"minLength": 2,
"maxLength": 255
},
"value": {
"description": "Value",
"example": "404",
"type": "string",
"maxLength": 255
},
"meta": {
"type": "object"
}
},
"links": [
{
"title": "List",
"description": "Returns a list of Settings",
"href": "/settings",
"access": "private",
"method": "GET",
"rel": "self",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
},
"targetSchema": {
"type": "array",
"items": {
"$ref": "#/properties"
}
}
},
{
"title": "Update",
"description": "Updates a existing Setting",
"href": "/settings/{definitions.identity.example}",
"access": "private",
"method": "PUT",
"rel": "update",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
},
"schema": {
"type": "object",
"properties": {
"value": {
"$ref": "#/definitions/value"
},
"meta": {
"$ref": "#/definitions/meta"
}
}
},
"targetSchema": {
"properties": {
"$ref": "#/properties"
}
}
}
],
"properties": {
"id": {
"$ref": "#/definitions/id"
},
"name": {
"$ref": "#/definitions/description"
},
"description": {
"$ref": "#/definitions/description"
},
"value": {
"$ref": "#/definitions/value"
},
"meta": {
"$ref": "#/definitions/meta"
}
}
}

View File

@ -34,6 +34,9 @@
}, },
"access-lists": { "access-lists": {
"$ref": "endpoints/access-lists.json" "$ref": "endpoints/access-lists.json"
},
"settings": {
"$ref": "endpoints/settings.json"
} }
} }
} }

View File

@ -0,0 +1,9 @@
location {{ path }} {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }};
{{ advanced_config }}
}

View File

@ -10,10 +10,13 @@ server {
{{ advanced_config }} {{ advanced_config }}
{% if use_default_location %}
location / { location / {
{% include "_forced_ssl.conf" %} {% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
return 404; return 404;
} }
{% endif %}
} }
{% endif %} {% endif %}

View File

@ -0,0 +1,32 @@
# ------------------------------------------------------------
# Default Site
# ------------------------------------------------------------
{% if value == "congratulations" %}
# Skipping output, congratulations page configration is baked in.
{%- else %}
server {
listen 80 default;
server_name default-host.localhost;
access_log /data/logs/default_host.log combined;
{% include "_exploits.conf" %}
{%- if value == "404" %}
location / {
return 404;
}
{% endif %}
{%- if value == "redirect" %}
location / {
return 301 {{ meta.redirect }};
}
{%- endif %}
{%- if value == "html" %}
root /data/nginx/default_www;
location / {
try_files $uri /index.html;
}
{%- endif %}
}
{% endif %}

View File

@ -16,6 +16,10 @@ server {
{{ advanced_config }} {{ advanced_config }}
{{ locations }}
{% if use_default_location %}
location / { location / {
{%- if access_list_id > 0 -%} {%- if access_list_id > 0 -%}
# Access List # Access List
@ -35,5 +39,7 @@ server {
# Proxy! # Proxy!
include conf.d/include/proxy.conf; include conf.d/include/proxy.conf;
} }
{% endif %}
} }
{% endif %} {% endif %}

View File

@ -12,6 +12,7 @@ server {
{{ advanced_config }} {{ advanced_config }}
{% if use_default_location %}
location / { location / {
{% include "_forced_ssl.conf" %} {% include "_forced_ssl.conf" %}
{% include "_hsts.conf" %} {% include "_hsts.conf" %}
@ -22,5 +23,7 @@ server {
return 301 $scheme://{{ forward_domain_name }}; return 301 $scheme://{{ forward_domain_name }};
{% endif %} {% endif %}
} }
{% endif %}
} }
{% endif %} {% endif %}

View File

@ -662,5 +662,34 @@ module.exports = {
getHostStats: function () { getHostStats: function () {
return fetch('get', 'reports/hosts'); return fetch('get', 'reports/hosts');
} }
},
Settings: {
/**
* @param {String} setting_id
* @returns {Promise}
*/
getById: function (setting_id) {
return fetch('get', 'settings/' + setting_id);
},
/**
* @returns {Promise}
*/
getAll: function () {
return getAllObjects('settings');
},
/**
* @param {Object} data
* @param {Number} data.id
* @returns {Promise}
*/
update: function (data) {
let id = data.id;
delete data.id;
return fetch('put', 'settings/' + id, data);
}
} }
}; };

View File

@ -383,6 +383,36 @@ module.exports = {
} }
}, },
/**
* Settings
*/
showSettings: function () {
let controller = this;
if (Cache.User.isAdmin()) {
require(['./main', './settings/main'], (App, View) => {
controller.navigate('/settings');
App.UI.showAppContent(new View());
});
} else {
this.showDashboard();
}
},
/**
* Settings Item Form
*
* @param model
*/
showSettingForm: function (model) {
if (Cache.User.isAdmin()) {
if (model.get('id') === 'default-site') {
require(['./main', './settings/default-site/main'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
}
},
/** /**
* Logout * Logout
*/ */

View File

@ -7,10 +7,22 @@
<form> <form>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li> <li role="presentation" class="nav-item"><a href="#details" aria-controls="tab1" role="tab" data-toggle="tab" class="nav-link active"><i class="fe fe-zap"></i> <%- i18n('all-hosts', 'details') %></a></li>
<li role="presentation" class="nav-item"><a href="#locations" aria-controls="tab4" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-layers"></i> <%- i18n('all-hosts', 'locations') %></a></li>
<li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li> <li role="presentation" class="nav-item"><a href="#ssl-options" aria-controls="tab2" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-shield"></i> <%- i18n('str', 'ssl') %></a></li>
<li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></a></li> <li role="presentation" class="nav-item"><a href="#advanced" aria-controls="tab3" role="tab" data-toggle="tab" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('all-hosts', 'advanced') %></a></li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<!-- Locations -->
<div class="tab-pane" id="locations">
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-secondary add_location"><%- i18n('locations', 'new_location') %></button>
<div class="locations_container mt-3"></div>
</div>
</div>
</div>
<!-- Details --> <!-- Details -->
<div role="tabpanel" class="tab-pane active" id="details"> <div role="tabpanel" class="tab-pane active" id="details">
<div class="row"> <div class="row">
@ -152,6 +164,12 @@
<div role="tabpanel" class="tab-pane" id="advanced"> <div role="tabpanel" class="tab-pane" id="advanced">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<p>Nginx variables available to you are:</p>
<ul class="text-monospace">
<li>$server # Host/IP</li>
<li>$port # Port Number</li>
<li>$forward_scheme # http or https</li>
</ul>
<div class="form-group mb-0"> <div class="form-group mb-0">
<label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label> <label class="form-label"><%- i18n('all-hosts', 'advanced-config') %></label>
<textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea> <textarea name="advanced_config" rows="8" class="form-control text-monospace" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>

View File

@ -3,11 +3,14 @@
const Mn = require('backbone.marionette'); const Mn = require('backbone.marionette');
const App = require('../../main'); const App = require('../../main');
const ProxyHostModel = require('../../../models/proxy-host'); const ProxyHostModel = require('../../../models/proxy-host');
const ProxyLocationModel = require('../../../models/proxy-host-location');
const template = require('./form.ejs'); const template = require('./form.ejs');
const certListItemTemplate = require('../certificates-list-item.ejs'); const certListItemTemplate = require('../certificates-list-item.ejs');
const accessListItemTemplate = require('./access-list-item.ejs'); const accessListItemTemplate = require('./access-list-item.ejs');
const CustomLocation = require('./location');
const Helpers = require('../../../lib/helpers'); const Helpers = require('../../../lib/helpers');
require('jquery-serializejson'); require('jquery-serializejson');
require('selectize'); require('selectize');
@ -15,6 +18,8 @@ module.exports = Mn.View.extend({
template: template, template: template,
className: 'modal-dialog', className: 'modal-dialog',
locationsCollection: new ProxyLocationModel.Collection(),
ui: { ui: {
form: 'form', form: 'form',
domain_names: 'input[name="domain_names"]', domain_names: 'input[name="domain_names"]',
@ -22,6 +27,8 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button', buttons: '.modal-footer button',
cancel: 'button.cancel', cancel: 'button.cancel',
save: 'button.save', save: 'button.save',
add_location_btn: 'button.add_location',
locations_container:'.locations_container',
certificate_select: 'select[name="certificate_id"]', certificate_select: 'select[name="certificate_id"]',
access_list_select: 'select[name="access_list_id"]', access_list_select: 'select[name="access_list_id"]',
ssl_forced: 'input[name="ssl_forced"]', ssl_forced: 'input[name="ssl_forced"]',
@ -32,6 +39,10 @@ module.exports = Mn.View.extend({
letsencrypt: '.letsencrypt' letsencrypt: '.letsencrypt'
}, },
regions: {
locations_regions: '@ui.locations_container'
},
events: { events: {
'change @ui.certificate_select': function () { 'change @ui.certificate_select': function () {
let id = this.ui.certificate_select.val(); let id = this.ui.certificate_select.val();
@ -82,6 +93,13 @@ module.exports = Mn.View.extend({
} }
}, },
'click @ui.add_location_btn': function (e) {
e.preventDefault();
const model = new ProxyLocationModel.Model();
this.locationsCollection.add(model);
},
'click @ui.save': function (e) { 'click @ui.save': function (e) {
e.preventDefault(); e.preventDefault();
@ -93,6 +111,16 @@ module.exports = Mn.View.extend({
let view = this; let view = this;
let data = this.ui.form.serializeJSON(); let data = this.ui.form.serializeJSON();
// Add locations
data.locations = [];
this.locationsCollection.models.forEach((location) => {
data.locations.push(location.toJSON());
});
// Serialize collects path from custom locations
// This field must be removed from root object
delete data.path;
// Manipulate // Manipulate
data.forward_port = parseInt(data.forward_port, 10); data.forward_port = parseInt(data.forward_port, 10);
data.block_exploits = !!data.block_exploits; data.block_exploits = !!data.block_exploits;
@ -246,5 +274,20 @@ module.exports = Mn.View.extend({
if (typeof options.model === 'undefined' || !options.model) { if (typeof options.model === 'undefined' || !options.model) {
this.model = new ProxyHostModel.Model(); this.model = new ProxyHostModel.Model();
} }
this.locationsCollection = new ProxyLocationModel.Collection();
// Custom locations
this.showChildView('locations_regions', new CustomLocation.LocationCollectionView({
collection: this.locationsCollection
}));
// Check wether there are any location defined
if (options.model && Array.isArray(options.model.attributes.locations)) {
options.model.attributes.locations.forEach((location) => {
let m = new ProxyLocationModel.Model(location);
this.locationsCollection.add(m);
});
}
} }
}); });

View File

@ -0,0 +1,63 @@
<div class="location-block card">
<div class="card-body">
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="form-label"><%- i18n('locations', 'location_label') %> <span class="form-required">*</span></label>
<div class="row gutter-xs">
<div class="col">
<div class="input-group">
<span class="input-group-prepend">
<span class="input-group-text">location</span>
</span>
<input type="text" name="path" class="form-control model" value="<%- path %>" placeholder="<%- i18n('locations', 'path') %>" required>
</div>
</div>
<div class="col-auto">
<div class="selectgroup">
<label class="selectgroup-item">
<input type="checkbox" class="selectgroup-input">
<span class="selectgroup-button">
<i class="fe fe-settings"></i>
</span>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-3 col-md-3">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-scheme') %><span class="form-required">*</span></label>
<select name="forward_scheme" class="form-control custom-select model" placeholder="http">
<option value="http" <%- forward_scheme === 'http' ? 'selected' : '' %>>http</option>
<option value="https" <%- forward_scheme === 'https' ? 'selected' : '' %>>https</option>
</select>
</div>
</div>
<div class="col-sm-5 col-md-5">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-host') %><span class="form-required">*</span></label>
<input type="text" name="forward_host" class="form-control text-monospace model" placeholder="" value="<%- forward_host %>" autocomplete="off" maxlength="50" required>
</div>
</div>
<div class="col-sm-4 col-md-4">
<div class="form-group">
<label class="form-label"><%- i18n('proxy-hosts', 'forward-port') %> <span class="form-required">*</span></label>
<input name="forward_port" type="number" class="form-control text-monospace model" placeholder="80" value="<%- forward_port %>" required>
</div>
</div>
</div>
<div class="row config">
<div class="col-md-12">
<div class="form-group">
<textarea name="advanced_config" rows="8" class="form-control text-monospace model" placeholder="# <%- i18n('all-hosts', 'advanced-warning') %>"><%- advanced_config %></textarea>
</div>
</div>
</div>
<a href="#" class="card-link location-delete">
<i class="fa fa-trash"></i> <%- i18n('locations', 'delete') %>
</a>
</div>
</div>

View File

@ -0,0 +1,54 @@
const locationItemTemplate = require('./location-item.ejs');
const Mn = require('backbone.marionette');
const App = require('../../main');
const LocationView = Mn.View.extend({
template: locationItemTemplate,
className: 'location_block',
ui: {
toggle: 'input[type="checkbox"]',
config: '.config',
delete: '.location-delete'
},
events: {
'change @ui.toggle': function(el) {
if (el.target.checked) {
this.ui.config.show();
} else {
this.ui.config.hide();
}
},
'change .model': function (e) {
const map = {};
map[e.target.name] = e.target.value;
this.model.set(map);
},
'click @ui.delete': function () {
this.model.destroy();
}
},
onRender: function() {
$(this.ui.config).hide();
},
templateContext: function() {
return {
i18n: App.i18n
}
}
});
const LocationCollectionView = Mn.CollectionView.extend({
className: 'locations_container',
childView: LocationView
});
module.exports = {
LocationCollectionView,
LocationView
}

View File

@ -15,6 +15,7 @@ module.exports = AppRouter.default.extend({
'nginx/access': 'showNginxAccess', 'nginx/access': 'showNginxAccess',
'nginx/certificates': 'showNginxCertificates', 'nginx/certificates': 'showNginxCertificates',
'audit-log': 'showAuditLog', 'audit-log': 'showAuditLog',
'settings': 'showSettings',
'*default': 'showDashboard' '*default': 'showDashboard'
} }
}); });

View File

@ -0,0 +1,53 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%- i18n('settings', id) %></h5>
<button type="button" class="close cancel" aria-label="Close" data-dismiss="modal">&nbsp;</button>
</div>
<div class="modal-body">
<form>
<div class="row">
<div class="col-sm-12 col-md-12">
<div class="form-group">
<div class="form-label"><%- description %></div>
<div class="custom-controls-stacked">
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="congratulations" type="radio" required <%- value === 'congratulations' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-congratulations') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="404" type="radio" required <%- value === '404' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-404') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="redirect" type="radio" required <%- value === 'redirect' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-redirect') %></div>
</label>
<label class="custom-control custom-radio">
<input class="custom-control-input" name="value" value="html" type="radio" required <%- value === 'html' ? 'checked' : '' %>>
<div class="custom-control-label"><%- i18n('settings', 'default-site-html') %></div>
</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-redirect">
<div class="form-group">
<div class="form-label">Redirect to</div>
<input class="form-control redirect-input" name="meta[redirect]" placeholder="https://" type="url" value="<%- meta && typeof meta.redirect !== 'undefined' ? meta.redirect : '' %>">
</div>
</div>
<div class="col-sm-12 col-md-12 option-item option-html">
<div class="form-group">
<div class="form-label">HTML Content</div>
<textarea class="form-control text-monospace html-content" name="meta[html]" rows="6" placeholder="<!-- Enter your HTML here -->"><%- meta && typeof meta.html !== 'undefined' ? meta.html : '' %></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary cancel" data-dismiss="modal"><%- i18n('str', 'cancel') %></button>
<button type="button" class="btn btn-teal save"><%- i18n('str', 'save') %></button>
</div>
</div>

View File

@ -0,0 +1,71 @@
'use strict';
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./main.ejs');
require('jquery-serializejson');
require('selectize');
module.exports = Mn.View.extend({
template: template,
className: 'modal-dialog',
ui: {
form: 'form',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
options: '.option-item',
value: 'input[name="value"]',
redirect: '.redirect-input',
html: '.html-content'
},
events: {
'change @ui.value': function (e) {
let val = this.ui.value.filter(':checked').val();
this.ui.options.hide();
this.ui.options.filter('.option-' + val).show();
},
'click @ui.save': function (e) {
e.preventDefault();
let val = this.ui.value.filter(':checked').val();
// Clear redirect field before validation
if (val !== 'redirect') {
this.ui.redirect.val('').attr('required', false);
} else {
this.ui.redirect.attr('required', true);
}
this.ui.html.attr('required', val === 'html');
if (!this.ui.form[0].checkValidity()) {
$('<input type="submit">').hide().appendTo(this.ui.form).click().remove();
return;
}
let view = this;
let data = this.ui.form.serializeJSON();
data.id = this.model.get('id');
this.ui.buttons.prop('disabled', true).addClass('btn-disabled');
App.Api.Settings.update(data)
.then(result => {
view.model.set(result);
App.UI.closeModal();
})
.catch(err => {
alert(err.message);
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
});
}
},
onRender: function () {
this.ui.value.trigger('change');
}
});

View File

@ -0,0 +1,21 @@
<td>
<div><%- name %></div>
<div class="small text-muted">
<%- description %>
</div>
</td>
<td>
<div>
<% if (id === 'default-site') { %>
<%- i18n('settings', 'default-site-' + value) %>
<% } %>
</div>
</td>
<td class="text-right">
<div class="item-action dropdown">
<a href="#" data-toggle="dropdown" class="icon"><i class="fe fe-more-vertical"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a href="#" class="edit dropdown-item"><i class="dropdown-icon fe fe-edit"></i> <%- i18n('str', 'edit') %></a>
</div>
</div>
</td>

View File

@ -0,0 +1,25 @@
'use strict';
const Mn = require('backbone.marionette');
const App = require('../../main');
const template = require('./item.ejs');
module.exports = Mn.View.extend({
template: template,
tagName: 'tr',
ui: {
edit: 'a.edit'
},
events: {
'click @ui.edit': function (e) {
e.preventDefault();
App.Controller.showSettingForm(this.model);
}
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});

View File

@ -0,0 +1,8 @@
<thead>
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('str', 'value') %></th>
<th>&nbsp;</th>
</thead>
<tbody>
<!-- items -->
</tbody>

View File

@ -0,0 +1,29 @@
'use strict';
const Mn = require('backbone.marionette');
const ItemView = require('./item');
const template = require('./main.ejs');
const TableBody = Mn.CollectionView.extend({
tagName: 'tbody',
childView: ItemView
});
module.exports = Mn.View.extend({
tagName: 'table',
className: 'table table-hover table-outline table-vcenter text-nowrap card-table',
template: template,
regions: {
body: {
el: 'tbody',
replaceElement: true
}
},
onRender: function () {
this.showChildView('body', new TableBody({
collection: this.collection
}));
}
});

View File

@ -0,0 +1,14 @@
<div class="card">
<div class="card-status bg-teal"></div>
<div class="card-header">
<h3 class="card-title"><%- i18n('settings', 'title') %></h3>
</div>
<div class="card-body no-padding min-100">
<div class="dimmer active">
<div class="loader"></div>
<div class="dimmer-content list-region">
<!-- List Region -->
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
'use strict';
const Mn = require('backbone.marionette');
const App = require('../main');
const SettingModel = require('../../models/setting');
const ListView = require('./list/main');
const ErrorView = require('../error/main');
const template = require('./main.ejs');
module.exports = Mn.View.extend({
id: 'settings',
template: template,
ui: {
list_region: '.list-region',
add: '.add-item',
dimmer: '.dimmer'
},
regions: {
list_region: '@ui.list_region'
},
onRender: function () {
let view = this;
App.Api.Settings.getAll()
.then(response => {
if (!view.isDestroyed() && response && response.length) {
view.showChildView('list_region', new ListView({
collection: new SettingModel.Collection(response)
}));
}
})
.catch(err => {
view.showChildView('list_region', new ErrorView({
code: err.code,
message: err.message,
retry: function () {
App.Controller.showSettings();
}
}));
console.error(err);
})
.then(() => {
view.ui.dimmer.removeClass('active');
});
}
});

View File

@ -42,6 +42,9 @@
<li class="nav-item"> <li class="nav-item">
<a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a> <a href="/audit-log" class="nav-link"><i class="fe fe-book-open"></i> <%- i18n('audit-log', 'title') %></a>
</li> </li>
<li class="nav-item">
<a href="/settings" class="nav-link"><i class="fe fe-settings"></i> <%- i18n('settings', 'title') %></a>
</li>
<% } %> <% } %>
</ul> </ul>
</div> </div>

View File

@ -31,7 +31,8 @@
"online": "Online", "online": "Online",
"offline": "Offline", "offline": "Offline",
"unknown": "Unknown", "unknown": "Unknown",
"expires": "Expires" "expires": "Expires",
"value": "Value"
}, },
"login": { "login": {
"title": "Login to your account" "title": "Login to your account"
@ -81,7 +82,14 @@
"advanced-warning": "Enter your custom Nginx configuration here at your own risk!", "advanced-warning": "Enter your custom Nginx configuration here at your own risk!",
"advanced-config": "Custom Nginx Configuration", "advanced-config": "Custom Nginx Configuration",
"hsts-enabled": "HSTS Enabled", "hsts-enabled": "HSTS Enabled",
"hsts-subdomains": "HSTS Subdomains" "hsts-subdomains": "HSTS Subdomains",
"locations": "Custom locations"
},
"locations": {
"new_location": "Add location",
"path": "/path",
"location_label": "Define location",
"delete": "Delete"
}, },
"ssl": { "ssl": {
"letsencrypt": "Let's Encrypt", "letsencrypt": "Let's Encrypt",
@ -222,6 +230,14 @@
"meta-title": "Details for Event", "meta-title": "Details for Event",
"view-meta": "View Details", "view-meta": "View Details",
"date": "Date" "date": "Date"
},
"settings": {
"title": "Settings",
"default-site": "Default Site",
"default-site-congratulations": "Congratulations Page",
"default-site-404": "404 Page",
"default-site-html": "Custom Page",
"default-site-redirect": "Redirect"
} }
} }
} }

View File

@ -0,0 +1,37 @@
'use strict';
const Backbone = require('backbone');
const model = Backbone.Model.extend({
idAttribute: 'id',
defaults: function() {
return {
opened: false,
path: '',
advanced_config: '',
forward_scheme: 'http',
forward_host: '',
forward_port: '80'
}
},
toJSON() {
const r = Object.assign({}, this.attributes);
delete r.opened;
return r;
},
toggleVisibility: function () {
this.save({
opened: !this.get('opened')
});
}
})
module.exports = {
Model: model,
Collection: Backbone.Collection.extend({
model
})
}

View File

@ -0,0 +1,25 @@
'use strict';
const _ = require('underscore');
const Backbone = require('backbone');
const model = Backbone.Model.extend({
idAttribute: 'id',
defaults: function () {
return {
id: undefined,
name: '',
description: '',
value: null,
meta: []
};
}
});
module.exports = {
Model: model,
Collection: Backbone.Collection.extend({
model: model
})
};