Merge branch 'master' into stable
# Conflicts: # .github/workflows/docker-publish.yml # README.md # assets/tpl/admin_index.html # assets/tpl/user_index.html # cmd/wg-portal/main.go # docker-compose.yml # go.mod # go.sum # internal/common/util.go # internal/server/docs/docs.go # internal/server/handlers_common.go # internal/server/server.go # internal/wireguard/peermanager.go
@ -1,92 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build-latest:
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-latest-v4-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
make build-dependencies
|
||||
- save_cache:
|
||||
key: go-mod-latest-v4-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "~/go/pkg/mod"
|
||||
- run:
|
||||
name: Build AMD64
|
||||
command: |
|
||||
VERSION=$CIRCLE_BRANCH
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
|
||||
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-amd64
|
||||
- run:
|
||||
name: Install Cross-Platform Dependencies
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-arm-linux-gnueabihf libc6-dev-armhf-cross
|
||||
sudo ln -s /usr/include/asm-generic /usr/include/asm
|
||||
- run:
|
||||
name: Build ARM64
|
||||
command: |
|
||||
VERSION=$CIRCLE_BRANCH
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
|
||||
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm64
|
||||
- run:
|
||||
name: Build ARM
|
||||
command: |
|
||||
VERSION=$CIRCLE_BRANCH
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
|
||||
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build-arm
|
||||
- store_artifacts:
|
||||
path: ~/repo/dist
|
||||
- run:
|
||||
name: "Publish Release on GitHub"
|
||||
command: |
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then
|
||||
go install github.com/tcnksm/ghr@latest
|
||||
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace $CIRCLE_TAG ~/repo/dist
|
||||
fi
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
- image: cimg/go:1.19
|
||||
build-118: # just to validate compatibility with minimum go version
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-118-v4-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
make build-dependencies
|
||||
- save_cache:
|
||||
key: go-mod-118-v4-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "~/go/pkg/mod"
|
||||
- run:
|
||||
name: Build
|
||||
command: |
|
||||
VERSION=$CIRCLE_BRANCH
|
||||
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
|
||||
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
|
||||
working_directory: ~/repo118
|
||||
docker:
|
||||
- image: cimg/go:1.18
|
||||
|
||||
workflows:
|
||||
build-and-release:
|
||||
jobs:
|
||||
#--------------- BUILD ---------------#
|
||||
- build-latest:
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
||||
- build-118:
|
||||
requires:
|
||||
- build-latest
|
||||
filters:
|
||||
tags:
|
||||
only: /^v.*/
|
14
.dockerignore
Normal file
@ -0,0 +1,14 @@
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# Allow backend files
|
||||
!cmd/
|
||||
!internal/
|
||||
!go.mod
|
||||
!go.sum
|
||||
|
||||
# Allow frontend files
|
||||
!frontend/
|
||||
|
||||
# Ignore node_modules
|
||||
**/node_modules/
|
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve WG-Portal
|
||||
labels: bug
|
||||
|
||||
---
|
||||
<!-- Tip: you can use code blocks
|
||||
for better formatting of yaml config or logs
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
```
|
||||
|
||||
```console
|
||||
logs here
|
||||
``` -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Steps to reproduce**
|
||||
<!--Steps to reproduce the bug should be clear and easily reproducible to help people
|
||||
gain an understanding of the problem.-->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
- Application version: v
|
||||
- Install method: binary/docker/helm/sources
|
||||
<!-- - OS: -->
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels: 'enhancement'
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
30
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
golang:
|
||||
patterns:
|
||||
- golang.org*
|
||||
gorm:
|
||||
patterns:
|
||||
- gorm.io*
|
||||
patch:
|
||||
update-types:
|
||||
- patch
|
18
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
## Problem Statement
|
||||
|
||||
What is the problem you're trying to solve?
|
||||
|
||||
## Related Issue
|
||||
|
||||
Fixes #...
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
How do you like to solve the issue and why?
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Commits are signed with `git commit --signoff`
|
||||
- [ ] Changes have reasonable test coverage
|
||||
- [ ] Tests pass with `make test`
|
||||
- [ ] Helm docs are up-to-date with `make helm-docs`
|
75
.github/workflows/chart.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
# Publish chart to the GitHub Container Registry (GHCR) on push to master
|
||||
# Run the following tests on PRs:
|
||||
# - Check if chart's documentation is up to date
|
||||
# - Check chart linting
|
||||
# - Check chart installation in a Kind cluster
|
||||
# - Check chart packaging
|
||||
|
||||
name: Chart
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths: ['deploy/helm/**']
|
||||
push:
|
||||
branches: [master]
|
||||
paths: ['deploy/helm/**']
|
||||
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check docs
|
||||
run: |
|
||||
make helm-docs
|
||||
if ! git diff --exit-code; then
|
||||
echo "error::Documentation is not up to date. Please run helm-docs and commit changes."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ct lint requires Python 3.x to run following packages:
|
||||
# - yamale (https://github.com/23andMe/Yamale)
|
||||
# - yamllint (https://github.com/adrienverge/yamllint)
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- uses: helm/chart-testing-action@v2
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --config ct.yaml
|
||||
|
||||
- uses: nolar/setup-k3d-k3s@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run chart-testing (install)
|
||||
run: ct install --config ct.yaml
|
||||
|
||||
- name: Check chart packaging
|
||||
run: helm package deploy/helm
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Package helm chart
|
||||
run: helm package deploy/helm
|
||||
|
||||
- name: Push chart to GHCR
|
||||
run: helm push wg-portal-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts
|
67
.github/workflows/codeql-analysis.yml
vendored
@ -1,67 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '35 15 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go', 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
170
.github/workflows/docker-publish.yml
vendored
@ -1,139 +1,125 @@
|
||||
name: Docker
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [ master, stable ]
|
||||
branches: [master, stable, legacy]
|
||||
# Publish vX.X.X tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
tags: ["v*.*.*"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-dockerhub:
|
||||
name: Push Docker image to Docker Hub
|
||||
build-n-push:
|
||||
name: Build and Push
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
|
||||
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
|
||||
id: get_version
|
||||
run: echo "BUILD_VERSION=${GITHUB_REF_NAME}-${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: wgportal/wg-portal
|
||||
flavor: |
|
||||
latest=false
|
||||
prefix=
|
||||
suffix=
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
build-args: |
|
||||
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
|
||||
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
|
||||
|
||||
build-github:
|
||||
name: Push Docker image to Github Container Registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
|
||||
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
|
||||
id: get_version
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
wgportal/wg-portal
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
latest=false
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=ref,event=branch
|
||||
# semver tags, without v prefix
|
||||
type=semver,pattern={{version}}
|
||||
# major and major.minor tags are not available for alpha or beta releases
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
type=semver,pattern={{major}}
|
||||
# add v{{major}} tag, even for beta or release-canidate releases
|
||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
# add {{major}} tag, even for beta releases or release-canidate releases
|
||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
build-args: |
|
||||
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
|
||||
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
|
||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
||||
|
||||
- name: Export binaries from images
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
target: binaries
|
||||
outputs: type=local,dest=./binaries
|
||||
build-args: |
|
||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
||||
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
for file in binaries/linux*/wg-portal; do
|
||||
mv $file binaries/wg-portal_$(basename $(dirname $file))
|
||||
done
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: binaries/wg-portal_linux*
|
||||
retention-days: 10
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-n-push
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'wg-portal_linux*'
|
||||
generate_release_notes: true
|
||||
|
33
.github/workflows/pages.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: github-pages
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags:
|
||||
- 'v*'
|
||||
- '!v*-alpha*'
|
||||
- '!v*-beta*'
|
||||
- '!v*-rc*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
|
||||
- name: Publish documentation
|
||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||
env:
|
||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
14
.gitignore
vendored
@ -28,12 +28,16 @@
|
||||
out/
|
||||
dist/
|
||||
data/
|
||||
docker_images/
|
||||
ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
||||
sqlite.db
|
||||
node_modules/
|
||||
/config.yml
|
||||
/config.yaml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
# ignore local frontend dist directory
|
||||
internal/app/api/core/frontend-dist
|
||||
# mkdocs output directory
|
||||
site/
|
||||
|
11
.run/swag_build_tool.run.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="swag_build_tool" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/cmd/api_build_tool" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/internal/ports/api/build_tool/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
17
.run/wg-portal-migrate.run.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="wg-portal-migrate" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="-migrateFrom=wg_portal.db" />
|
||||
<envs>
|
||||
<env name="SESSION_SECRET" value="extremlybad" />
|
||||
<env name="LOG_LEVEL" value="trace" />
|
||||
</envs>
|
||||
<sudo value="true" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
16
.run/wg-portal.run.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="wg-portal" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="wg-portal" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<envs>
|
||||
<env name="SESSION_SECRET" value="extremlybad" />
|
||||
<env name="LOG_LEVEL" value="trace" />
|
||||
</envs>
|
||||
<sudo value="true" />
|
||||
<kind value="PACKAGE" />
|
||||
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
|
||||
<directory value="$PROJECT_DIR$" />
|
||||
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
96
Dockerfile
@ -1,53 +1,69 @@
|
||||
# Dockerfile References: https://docs.docker.com/engine/reference/builder/
|
||||
# This dockerfile uses a multi-stage build system to reduce the image footprint.
|
||||
|
||||
######-
|
||||
# Start from the latest golang base image as builder image (only used to compile the code)
|
||||
######-
|
||||
FROM golang:1.18 as builder
|
||||
|
||||
ARG BUILD_IDENTIFIER
|
||||
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
|
||||
|
||||
ARG BUILD_VERSION
|
||||
ENV ENV_BUILD_VERSION=$BUILD_VERSION
|
||||
|
||||
# populated by BuildKit
|
||||
ARG TARGETPLATFORM
|
||||
ENV ENV_TARGETPLATFORM=$TARGETPLATFORM
|
||||
|
||||
RUN mkdir /build
|
||||
|
||||
# Copy the source from the current directory to the Working Directory inside the container
|
||||
ADD . /build/
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
######
|
||||
# Build frontend
|
||||
######
|
||||
FROM --platform=${BUILDPLATFORM} node:lts-alpine AS frontend
|
||||
# Set the working directory
|
||||
WORKDIR /build
|
||||
# Download dependencies
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
# Set dist output directory
|
||||
ENV DIST_OUT_DIR="dist"
|
||||
# Copy the sources to the working directory
|
||||
COPY frontend .
|
||||
# Build the frontend
|
||||
RUN npm run build
|
||||
|
||||
# Build the Go app
|
||||
RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platform $ENV_TARGETPLATFORM"; make build
|
||||
######
|
||||
# Build backend
|
||||
######
|
||||
FROM --platform=${BUILDPLATFORM} golang:1.24-alpine AS builder
|
||||
# Set the working directory
|
||||
WORKDIR /build
|
||||
# Download dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
# Copy the sources to the working directory
|
||||
COPY ./cmd ./cmd
|
||||
COPY ./internal ./internal
|
||||
# Copy the frontend build result
|
||||
COPY --from=frontend /build/dist/ ./internal/app/api/core/frontend-dist/
|
||||
# Set the build version from arguments
|
||||
ARG BUILD_VERSION
|
||||
# Split to cross-platform build
|
||||
ARG TARGETARCH
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o /build/dist/wg-portal \
|
||||
-ldflags "-w -s -extldflags '-static' -X 'github.com/h44z/wg-portal/internal.Version=${BUILD_VERSION}'" \
|
||||
-tags netgo \
|
||||
cmd/wg-portal/main.go
|
||||
|
||||
######-
|
||||
# Here starts the main image
|
||||
######-
|
||||
FROM scratch
|
||||
######
|
||||
# Export binaries
|
||||
######
|
||||
FROM scratch AS binaries
|
||||
COPY --from=builder /build/dist/wg-portal /
|
||||
|
||||
######
|
||||
# Final image
|
||||
######
|
||||
FROM alpine:3.19
|
||||
# Install OS-level dependencies
|
||||
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
|
||||
# Setup timezone
|
||||
ENV TZ=Europe/Vienna
|
||||
|
||||
# Import linux stuff from builder.
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
|
||||
ENV TZ=UTC
|
||||
# Copy binaries
|
||||
COPY --from=builder /build/dist/wg-portal /app/wg-portal
|
||||
COPY --from=builder /build/dist/hc /app/hc
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Expose default ports for metrics, web and wireguard
|
||||
EXPOSE 8787/tcp
|
||||
EXPOSE 8888/tcp
|
||||
EXPOSE 51820/udp
|
||||
# the database and config file can be mounted from the host
|
||||
VOLUME [ "/app/data", "/app/config" ]
|
||||
# Command to run the executable
|
||||
CMD [ "/app/wg-portal" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]
|
||||
ENTRYPOINT [ "/app/wg-portal" ]
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 Christoph Haas
|
||||
Copyright (c) 2020-2023 Christoph Haas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
56
Makefile
@ -5,6 +5,7 @@ GOFILES:=$(shell go list ./... | grep -v /vendor/)
|
||||
BUILDDIR=dist
|
||||
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
|
||||
IMAGE=h44z/wg-portal
|
||||
NPMCMD=npm
|
||||
|
||||
all: help
|
||||
|
||||
@ -25,7 +26,6 @@ help:
|
||||
#> codegen: Re-generate autogenerated files (like API docs)
|
||||
.PHONY: codegen
|
||||
codegen: $(SUBDIRS)
|
||||
$(GOCMD) install github.com/swaggo/swag/cmd/swag@v1.8.10
|
||||
cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/
|
||||
$(GOCMD) fmt internal/server/docs/docs.go
|
||||
|
||||
@ -75,55 +75,67 @@ clean:
|
||||
#< build: Build all executables (architecture depends on build system)
|
||||
.PHONY: build
|
||||
build: build-dependencies
|
||||
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
|
||||
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
|
||||
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||
-tags netgo \
|
||||
cmd/wg-portal/main.go
|
||||
|
||||
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc \
|
||||
-ldflags "-w -s -extldflags \"-static\"" \
|
||||
cmd/hc/main.go
|
||||
|
||||
#< build-amd64: Build all executables for AMD64
|
||||
.PHONY: build-amd64
|
||||
build-amd64: build-dependencies
|
||||
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
|
||||
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
|
||||
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||
-tags netgo \
|
||||
cmd/wg-portal/main.go
|
||||
|
||||
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc-amd64 \
|
||||
-ldflags "-w -s -extldflags \"-static\"" \
|
||||
cmd/hc/main.go
|
||||
|
||||
#< build-arm64: Build all executables for ARM64
|
||||
.PHONY: build-arm64
|
||||
build-arm64: build-dependencies
|
||||
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
|
||||
CGO_ENABLED=0 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
|
||||
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||
-tags netgo \
|
||||
cmd/wg-portal/main.go
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/hc-arm64 \
|
||||
-ldflags "-w -s -extldflags \"-static\"" \
|
||||
cmd/hc/main.go
|
||||
|
||||
#< build-arm: Build all executables for ARM32
|
||||
.PHONY: build-arm
|
||||
build-arm: build-dependencies
|
||||
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
|
||||
CGO_ENABLED=0 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
|
||||
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||
-tags netgo \
|
||||
cmd/wg-portal/main.go
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/hc-arm \
|
||||
-ldflags "-w -s -extldflags \"-static\"" \
|
||||
cmd/hc/main.go
|
||||
|
||||
#< build-dependencies: Generate the output directory for compiled executables and download dependencies
|
||||
.PHONY: build-dependencies
|
||||
build-dependencies:
|
||||
@$(GOCMD) mod download -x
|
||||
@mkdir -p $(BUILDDIR)
|
||||
cp scripts/wg-portal.service $(BUILDDIR)
|
||||
cp scripts/wg-portal.env $(BUILDDIR)
|
||||
|
||||
#< frontend: Build Vue.js frontend
|
||||
frontend: frontend-dependencies
|
||||
cd frontend; $(NPMCMD) run build
|
||||
|
||||
#< frontend-dependencies: Generate the output directory for compiled executables and download frontend dependencies
|
||||
.PHONY: frontend-dependencies
|
||||
frontend-dependencies:
|
||||
@mkdir -p $(BUILDDIR)
|
||||
cd frontend; $(NPMCMD) install
|
||||
|
||||
#< build-docker: Build a docker image on the current host system
|
||||
.PHONY: build-docker
|
||||
build-docker:
|
||||
docker build --progress=plain \
|
||||
--build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \
|
||||
--build-arg TARGETPLATFORM=unknown . \
|
||||
-t h44z/wg-portal:local
|
||||
|
||||
#< helm-docs: Generate the helm chart documentation
|
||||
.PHONY: helm-docs
|
||||
helm-docs:
|
||||
docker run --rm --volume "${PWD}/deploy:/helm-docs" -u "$$(id -u)" jnorwood/helm-docs -s file
|
||||
|
||||
#< run-mkdocs: Run a local instance of MkDocs
|
||||
.PHONY: run-mkdocs
|
||||
run-mkdocs:
|
||||
python -m venv venv; source venv/bin/activate; pip install mike cairosvg mkdocs-material mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||
venv/bin/mkdocs serve
|
||||
|
@ -1,51 +0,0 @@
|
||||
# WireGuard Portal on Raspberry Pi
|
||||
|
||||
This readme only contains a detailed explanation of how to set up the WireGuard Portal service on a raspberry pi (>= 3).
|
||||
|
||||
## Setup
|
||||
|
||||
You can either download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases) or use Docker images for ARM.
|
||||
If you want to build the binary yourself, use the following building instructions.
|
||||
|
||||
### Building
|
||||
This section describes how to build the WireGuard Portal code.
|
||||
To compile the final binary, use the Makefile provided in the repository.
|
||||
As WireGuard Portal is written in Go, **golang >= 1.18** must be installed prior to building.
|
||||
If you want to cross compile ARM binaries from AMD64 systems, install *arm-linux-gnueabi-gcc* (armv7) or *aarch64-linux-gnu-gcc* (arm64).
|
||||
|
||||
```
|
||||
# for 64 bit OS
|
||||
make build-arm64
|
||||
|
||||
# for 32 bit OS
|
||||
make build-arm
|
||||
```
|
||||
|
||||
The compiled binary and all necessary assets will be located in the dist folder.
|
||||
|
||||
### Service setup
|
||||
|
||||
- Copy the contents from the dist folder (or from the downloaded zip file) to `/opt/wg-portal`. You can choose a different path as well, but make sure to update the systemd service file accordingly.
|
||||
- Update the provided systemd `wg-portal.service` file:
|
||||
- Make sure that the binary matches the system architecture.
|
||||
- There are three pre-build binaries available: wg-portal-**amd64**, wg-portal-**arm64** and wg-portal-**arm**.
|
||||
- For a raspberry pi use the arm binary if you are using armv7l architecture. If armv8 is used, the arm64 version should work.
|
||||
- Make sure that the paths to the binary and the working directory are set correctly (defaults to /opt/wg-portal/wg-portal-amd64):
|
||||
- ConditionPathExists
|
||||
- WorkingDirectory
|
||||
- ExecStart
|
||||
- EnvironmentFile
|
||||
- Update environment variables in the `wg-portal.env` file to fit your needs
|
||||
- Make sure that the binary application file is executable
|
||||
- `sudo chmod +x /opt/wg-portal/wg-portal-*`
|
||||
- Link the system service file to the correct folder:
|
||||
- `sudo ln -s /opt/wg-portal/wg-portal.service /etc/systemd/system/wg-portal.service`
|
||||
- Reload the systemctl daemon:
|
||||
- `sudo systemctl daemon-reload`
|
||||
|
||||
### Manage the service
|
||||
Once the service has been setup, you can simply manage the service using `systemctl`:
|
||||
- Enable on startup: `systemctl enable wg-portal.service`
|
||||
- Start: `systemctl start wg-portal.service`
|
||||
- Stop: `systemctl stop wg-portal.service`
|
||||
- Status: `systemctl status wg-portal.service`
|
277
README.md
@ -1,260 +1,67 @@
|
||||
# WireGuard Portal (v1)
|
||||
# WireGuard Portal v2
|
||||
|
||||
[](https://travis-ci.com/h44z/wg-portal)
|
||||
[](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/h44z/wg-portal)
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/h44z/wg-portal/)
|
||||
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
||||
|
||||
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
|
||||
## Introduction
|
||||
<!-- Text from this line # is included in docs/documentation/overview.md -->
|
||||
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
|
||||
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
||||
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
||||
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
|
||||
connections.
|
||||
|
||||
The configuration portal currently supports using SQLite and MySQL as a user source for authentication and profile data.
|
||||
It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
|
||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Postgres), OAuth or LDAP
|
||||
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
||||
|
||||
## Features
|
||||
* Self-hosted and web based
|
||||
* Automatically select IP from the network pool assigned to client
|
||||
* QR-Code for convenient mobile client configuration
|
||||
* Sent email to client with QR-code and client config
|
||||
* Enable / Disable clients seamlessly
|
||||
* Generation of `wgX.conf` after any modification
|
||||
* IPv6 ready
|
||||
* User authentication (SQLite/MySQL and LDAP)
|
||||
* Dockerized
|
||||
* Responsive template
|
||||
* One single binary
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* REST API for management and client deployment
|
||||
* Peer Expiry Feature
|
||||
|
||||

|
||||
* Self-hosted - the whole application is a single binary
|
||||
* Responsive multi-language web UI written in Vue.js
|
||||
* Automatically selects IP from the network pool assigned to the client
|
||||
* QR-Code for convenient mobile client configuration
|
||||
* Sends email to the client with QR-code and client config
|
||||
* Enable / Disable clients seamlessly
|
||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
||||
* User authentication (database, OAuth, or LDAP)
|
||||
* IPv6 ready
|
||||
* Docker ready
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* Peer Expiry Feature
|
||||
* Handles route and DNS settings like wg-quick does
|
||||
* Exposes Prometheus metrics for monitoring and alerting
|
||||
* REST API for management and client deployment
|
||||
* Webhook for custom actions on peer, interface, or user updates
|
||||
|
||||
## Setup
|
||||
Make sure that your host system has at least one WireGuard interface (for example wg0) available.
|
||||
If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started.
|
||||
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
||||

|
||||
|
||||
### Docker
|
||||
The easiest way to run WireGuard Portal is to use the [Docker image](https://hub.docker.com/r/wgportal/wg-portal) provided.
|
||||
## Documentation
|
||||
|
||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the image is now available on Docker Hub:
|
||||
https://hub.docker.com/r/wgportal/wg-portal
|
||||
|
||||
> :warning: **HINT**: the *latest* tag always refers to the master branch and might contain unstable or incompatible code!
|
||||
> For production use a fixed version or use the *stable* tag.
|
||||
|
||||
Docker Compose snippet with some sample configuration values:
|
||||
```
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v1
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
- '8123:8123'
|
||||
environment:
|
||||
# WireGuard Settings
|
||||
- WG_DEVICES=wg0
|
||||
- WG_DEFAULT_DEVICE=wg0
|
||||
- WG_CONFIG_PATH=/etc/wireguard
|
||||
# Core Settings
|
||||
- EXTERNAL_URL=https://vpn.company.com
|
||||
- WEBSITE_TITLE=WireGuard VPN
|
||||
- COMPANY_NAME=Your Company Name
|
||||
- ADMIN_USER=admin@domain.com
|
||||
- ADMIN_PASS=supersecret
|
||||
# Mail Settings
|
||||
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
|
||||
- EMAIL_HOST=10.10.10.10
|
||||
- EMAIL_PORT=25
|
||||
# LDAP Settings
|
||||
- LDAP_ENABLED=true
|
||||
- LDAP_URL=ldap://srv-ad01.company.local:389
|
||||
- LDAP_BASEDN=DC=COMPANY,DC=LOCAL
|
||||
- LDAP_USER=ldap_wireguard@company.local
|
||||
- LDAP_PASSWORD=supersecretldappassword
|
||||
- LDAP_ADMIN_GROUP=CN=WireGuardAdmins,OU=Users,DC=COMPANY,DC=LOCAL
|
||||
```
|
||||
Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration.
|
||||
If needed, please make sure to back up your files from ```/etc/wireguard```.
|
||||
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L58).
|
||||
|
||||
### Standalone
|
||||
For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.18 or higher has to be installed to build WireGuard Portal.
|
||||
|
||||
```shell
|
||||
# show all possible make commands
|
||||
make
|
||||
|
||||
# build wg-portal for current system architecture
|
||||
make build
|
||||
```
|
||||
|
||||
The compiled binary will be located in the dist folder.
|
||||
A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
|
||||
|
||||
To build the Docker image, Docker (> 20.x) with buildx is required. If you want to build cross-platform images, you need to install qemu.
|
||||
On arch linux for example install: `docker-buildx qemu-user-static qemu-user-static-binfmt`.
|
||||
|
||||
Once the Docker setup is completed, create a new buildx builder:
|
||||
```shell
|
||||
docker buildx create --name wgportalbuilder --platform linux/arm/v7,linux/arm64,linux/amd64
|
||||
docker buildx use wgportalbuilder
|
||||
docker buildx inspect --bootstrap
|
||||
```
|
||||
Now you can compile the Docker image:
|
||||
```shell
|
||||
# multi platform build, can only be exported to tar archives
|
||||
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 --output type=local,dest=docker_images \
|
||||
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
|
||||
|
||||
|
||||
# image for current platform only (same as docker build)
|
||||
docker buildx build --load \
|
||||
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
|
||||
```
|
||||
|
||||
## Configuration
|
||||
You can configure WireGuard Portal using either environment variables or a yaml configuration file.
|
||||
The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable.
|
||||
It is possible to override the configuration filepath using the environment variable **CONFIG_FILE**.
|
||||
For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
|
||||
|
||||
### Configuration Options
|
||||
The following configuration options are available:
|
||||
|
||||
| environment | yaml | yaml_parent | default_value | description |
|
||||
|----------------------------|-------------------------|-------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. |
|
||||
| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. |
|
||||
| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
|
||||
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
|
||||
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
|
||||
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
|
||||
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
|
||||
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
|
||||
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
|
||||
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
|
||||
| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
|
||||
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
|
||||
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
|
||||
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
|
||||
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
|
||||
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
||||
| DATABASE_HOST | host | database | | The mysql server address. |
|
||||
| DATABASE_PORT | port | database | | The mysql server port. |
|
||||
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
|
||||
| DATABASE_USERNAME | user | database | | The mysql user. |
|
||||
| DATABASE_PASSWORD | password | database | | The mysql password. |
|
||||
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
|
||||
| EMAIL_PORT | port | email | 25 | The email server port. |
|
||||
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
|
||||
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
|
||||
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
|
||||
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
|
||||
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
|
||||
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
|
||||
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
|
||||
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
|
||||
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
|
||||
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
|
||||
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
|
||||
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
|
||||
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
|
||||
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
|
||||
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
|
||||
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
|
||||
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
|
||||
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
|
||||
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. Users matching this filter will be synchronized with the WireGuard Portal database. |
|
||||
| LDAP_SYNC_GROUP_FILTER | syncGroupFilter | ldap | | The filter string for the LDAP groups, for example: (objectClass=group). The groups are used to recursively check for admin group member ship of users. |
|
||||
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
|
||||
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
|
||||
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
|
||||
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
|
||||
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
|
||||
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
|
||||
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
|
||||
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
|
||||
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
|
||||
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
|
||||
| LOG_JSON | | | false | Format log output as JSON. |
|
||||
| LOG_COLOR | | | true | Colorize log output. |
|
||||
| CONFIG_FILE | | | config.yml | The config file path. |
|
||||
|
||||
### Sample yaml configuration
|
||||
config.yml:
|
||||
```yaml
|
||||
core:
|
||||
listeningAddress: :8123
|
||||
externalUrl: https://wg-test.test.com
|
||||
adminUser: test@test.com
|
||||
adminPass: test
|
||||
editableKeys: true
|
||||
createDefaultPeer: false
|
||||
ldapEnabled: true
|
||||
mailFrom: WireGuard VPN <noreply@test.com>
|
||||
ldap:
|
||||
url: ldap://10.10.10.10:389
|
||||
dn: DC=test,DC=test
|
||||
startTLS: false
|
||||
user: wireguard@test.test
|
||||
pass: test
|
||||
adminGroup: CN=WireGuardAdmins,CN=Users,DC=test,DC=test
|
||||
database:
|
||||
typ: sqlite
|
||||
database: data/wg_portal.db
|
||||
email:
|
||||
host: smtp.gmail.com
|
||||
port: 587
|
||||
tls: true
|
||||
user: test@gmail.com
|
||||
pass: topsecret
|
||||
wg:
|
||||
devices:
|
||||
- wg0
|
||||
- wg1
|
||||
defaultDevice: wg0
|
||||
configDirectory: /etc/wireguard
|
||||
manageIPAddresses: true
|
||||
```
|
||||
|
||||
### RESTful API
|
||||
WireGuard Portal offers a RESTful API to interact with.
|
||||
The API is documented using OpenAPI 2.0, the Swagger UI can be found
|
||||
under the URL `http://<your wg-portal ip/domain>/swagger/index.html?displayOperationId=true`.
|
||||
|
||||
The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
|
||||
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||
|
||||
## What is out of scope
|
||||
* Creating or removing WireGuard (wgX) interfaces.
|
||||
* Generation or application of any `iptables` or `nftables` rules.
|
||||
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux.
|
||||
* Importing private keys of an existing WireGuard setup.
|
||||
|
||||
* Automatic generation or application of any `iptables` or `nftables` rules.
|
||||
* Support for operating systems other than linux.
|
||||
* Automatic import of private keys of an existing WireGuard setup.
|
||||
|
||||
## Application stack
|
||||
|
||||
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
|
||||
* [go-template, data-driven templates for generating textual output](https://golang.org/pkg/text/template/)
|
||||
* [Bootstrap, for the HTML templates](https://getbootstrap.com/)
|
||||
* [JQuery, for some nice JavaScript effects ;)](https://jquery.com/)
|
||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||
* [Vue.js](https://vuejs.org/), for the frontend
|
||||
|
||||
## License
|
||||
|
||||
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
|
||||
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||
|
||||
|
||||
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).
|
||||
> [!IMPORTANT]
|
||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
|
33
SECURITY.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Security Policy
|
||||
|
||||
If you believe you've found a security issue in one of the supported versions of *WireGuard Portal*, please report it to us as described below.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| v2.x | :white_check_mark: |
|
||||
| v1.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please do not report security vulnerabilities through public GitHub issues.
|
||||
|
||||
Instead, we encourage you to submit a report through GitHub [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
|
||||
If you prefer to submit a report without logging in to GitHub, please email *info (at) wgportal.org*.
|
||||
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
|
||||
|
||||
We prefer all communications to be in English.
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
- Type of issue (e.g. SQL injection, cross-site scripting, ...)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
Thank you for helping keep *WireGuard Portal* and its users safe!
|
@ -1,190 +0,0 @@
|
||||
// Lux 4.5.3
|
||||
// Bootswatch
|
||||
|
||||
|
||||
// Variables ===================================================================
|
||||
|
||||
$web-font-path: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap" !default;
|
||||
@import url($web-font-path);
|
||||
|
||||
// Navbar ======================================================================
|
||||
|
||||
.navbar {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
|
||||
&-nav {
|
||||
.nav-link {
|
||||
padding-top: .715rem;
|
||||
padding-bottom: .715rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-brand {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: theme-color("primary") !important;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
|
||||
&.navbar-fixed-top {
|
||||
border-width: 0 0 1px;
|
||||
}
|
||||
|
||||
&.navbar-bottom-top {
|
||||
border-width: 1px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
// Buttons =====================================================================
|
||||
|
||||
.btn {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
|
||||
&-sm {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
&,
|
||||
&:hover,
|
||||
&:not([disabled]):not(.disabled):active,
|
||||
&:focus {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
border-color: $gray-600;
|
||||
color: $gray-600;
|
||||
|
||||
&:not([disabled]):not(.disabled):hover,
|
||||
&:not([disabled]):not(.disabled):focus,
|
||||
&:not([disabled]):not(.disabled):active {
|
||||
background-color: $gray-400;
|
||||
border-color: $gray-400;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:not([disabled]):not(.disabled):focus {
|
||||
box-shadow: 0 0 0 .2rem rgba($gray-400, .5);
|
||||
}
|
||||
}
|
||||
|
||||
[class*="btn-outline-"] {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.border-secondary {
|
||||
border: 1px solid $gray-400 !important;
|
||||
}
|
||||
|
||||
// Typography ==================================================================
|
||||
|
||||
body {
|
||||
font-weight: 200;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $body-color !important;
|
||||
}
|
||||
|
||||
// Tables ======================================================================
|
||||
|
||||
th {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table {
|
||||
th,
|
||||
td {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
&-sm {
|
||||
th,
|
||||
td {
|
||||
padding: .75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forms =======================================================================
|
||||
|
||||
.custom-switch {
|
||||
.custom-control-label {
|
||||
&::after {
|
||||
top: add(.15625rem, 2px);
|
||||
left: add(-2.25rem, 2px);
|
||||
width: subtract(1rem, 4px);
|
||||
height: subtract(1rem, 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navs ========================================================================
|
||||
|
||||
.dropdown-menu {
|
||||
font-size: $font-size-sm;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
// Indicators ==================================================================
|
||||
|
||||
.badge {
|
||||
padding-top: .28rem;
|
||||
|
||||
&-pill {
|
||||
border-radius: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Containers ==================================================================
|
||||
|
||||
.list-group-item {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6 {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
&-title,
|
||||
&-header {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
// Lux 4.5.3
|
||||
// Bootswatch
|
||||
|
||||
//
|
||||
// Color system
|
||||
//
|
||||
|
||||
$white: #fff !default;
|
||||
$gray-100: #f8f9fa !default;
|
||||
$gray-200: #f7f7f9 !default;
|
||||
$gray-300: #eceeef !default;
|
||||
$gray-400: #ced4da !default;
|
||||
$gray-500: #adb5bd !default;
|
||||
$gray-600: #919aa1 !default;
|
||||
$gray-700: #55595c !default;
|
||||
$gray-800: #343a40 !default;
|
||||
$gray-900: #1a1a1a !default;
|
||||
$black: #000 !default;
|
||||
|
||||
$blue: #007bff !default;
|
||||
$indigo: #6610f2 !default;
|
||||
$purple: #6f42c1 !default;
|
||||
$pink: #e83e8c !default;
|
||||
$red: #d9534f !default;
|
||||
$orange: #fd7e14 !default;
|
||||
$yellow: #f0ad4e !default;
|
||||
$green: #4bbf73 !default;
|
||||
$teal: #20c997 !default;
|
||||
$cyan: #1f9bcf !default;
|
||||
|
||||
$primary: $gray-900 !default;
|
||||
$secondary: $white !default;
|
||||
$success: $green !default;
|
||||
$info: $cyan !default;
|
||||
$warning: $yellow !default;
|
||||
$danger: $red !default;
|
||||
$light: $white !default;
|
||||
$dark: $gray-800 !default;
|
||||
|
||||
$yiq-contrasted-threshold: 185 !default;
|
||||
|
||||
// Options
|
||||
|
||||
$enable-rounded: false !default;
|
||||
|
||||
// Body
|
||||
|
||||
$body-color: $gray-700 !default;
|
||||
|
||||
// Fonts
|
||||
|
||||
// stylelint-disable-next-line value-keyword-case
|
||||
$font-family-sans-serif: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
|
||||
$font-size-base: .875rem !default;
|
||||
$h1-font-size: 2rem !default;
|
||||
$h2-font-size: 1.75rem !default;
|
||||
$h3-font-size: 1.5rem !default;
|
||||
$h4-font-size: 1.25rem !default;
|
||||
$h5-font-size: 1rem !default;
|
||||
$h6-font-size: .75rem !default;
|
||||
$headings-font-weight: 600 !default;
|
||||
$headings-color: $gray-900 !default;
|
||||
|
||||
// Tables
|
||||
|
||||
$table-border-color: rgba(0, 0, 0, .05) !default;
|
||||
|
||||
// Buttons + Forms
|
||||
|
||||
$input-btn-border-width: 0 !default;
|
||||
|
||||
// Buttons
|
||||
|
||||
$btn-line-height: 1.5rem !default;
|
||||
$input-btn-padding-y: .75rem !default;
|
||||
$input-btn-padding-x: 1.5rem !default;
|
||||
$input-btn-padding-y-sm: .5rem !default;
|
||||
$input-btn-padding-x-sm: 1rem !default;
|
||||
$input-btn-padding-y-lg: 2rem !default;
|
||||
$input-btn-padding-x-lg: 2rem !default;
|
||||
$btn-font-weight: 600 !default;
|
||||
|
||||
// Forms
|
||||
|
||||
$input-line-height: 1.5 !default;
|
||||
$input-bg: $gray-200 !default;
|
||||
$input-disabled-bg: $gray-300 !default;
|
||||
$input-group-addon-bg: $gray-300 !default;
|
||||
|
||||
// Navbar
|
||||
|
||||
$navbar-padding-y: 1.5rem !default;
|
||||
$navbar-dark-hover-color: $white !default;
|
||||
$navbar-light-color: rgba($black, .3) !default;
|
||||
$navbar-light-hover-color: $gray-900 !default;
|
||||
$navbar-light-active-color: $gray-900 !default;
|
||||
|
||||
// Pagination
|
||||
|
||||
$pagination-border-color: transparent !default;
|
||||
$pagination-hover-border-color: $pagination-border-color !default;
|
||||
$pagination-disabled-border-color: $pagination-border-color !default;
|
||||
|
||||
// Breadcrumbs
|
||||
|
||||
$breadcrumb-bg: transparent !default;
|
5
assets/css/bootstrap-tokenfield.min.css
vendored
@ -1,5 +0,0 @@
|
||||
/*!
|
||||
* bootstrap-tokenfield
|
||||
* https://github.com/sliptree/bootstrap-tokenfield
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
||||
*/@-webkit-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{height:auto;min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px}
|
@ -1,118 +0,0 @@
|
||||
/* THEME STYLE */
|
||||
pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768px){body>.navbar-transparent{box-shadow:none}body>.navbar-transparent .navbar-nav>.open>a{box-shadow:none}}#home,#help{font-size:.9rem}#home .navbar,#help .navbar{background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);transition:box-shadow 200ms ease-in}#home .navbar-transparent,#help .navbar-transparent{background:none !important;box-shadow:none}#home .navbar-brand .nav-link,#help .navbar-brand .nav-link{display:inline-block;margin-right:-30px}#home .nav-link,#help .nav-link{text-transform:uppercase;font-weight:500;color:#fff}#home{padding-top:0}#home .btn{padding:.6rem .55rem .5rem;box-shadow:none;font-size:.7rem;font-weight:500}.bs-docs-section{margin-top:4em}.bs-docs-section .page-header h1{padding:2rem 0;font-size:3rem}.dropdown-menu.show[aria-labelledby="themes"]{display:-ms-flexbox;display:flex;width:420px;-ms-flex-wrap:wrap;flex-wrap:wrap}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item{width:33.333%}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item:first-child{width:100%}.bs-component{position:relative}.bs-component+.bs-component{margin-top:1rem}.bs-component .card{margin-bottom:1rem}.bs-component .modal{position:relative;top:auto;right:auto;left:auto;bottom:auto;z-index:1;display:block}.bs-component .modal-dialog{width:90%}.bs-component .popover{position:relative;display:inline-block;width:220px;margin:20px}.source-button{display:none;position:absolute;top:0;right:0;z-index:100;font-weight:700}.source-button:hover{cursor:pointer}.bs-component:hover .source-button{display:block}#source-modal pre{max-height:calc(100vh - 11rem);background-color:rgba(0,0,0,0.7);color:rgba(255,255,255,0.7)}.nav-tabs{margin-bottom:15px}.progress{margin-bottom:10px}#footer{margin:5em 0}#footer li{float:left;margin-right:1.5em;margin-bottom:1.5em}#footer p{clear:left;margin-bottom:0}.splash{padding:12em 0 6em;background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);color:#fff;text-align:center}.splash .logo{width:160px}.splash h1{font-size:3em;color:#fff}.splash #social{margin:2em 0 3em}.splash .alert{margin:2em 0;border:none}.splash .sponsor a{color:#fff}.section-tout{padding:6em 0 1em;border-bottom:1px solid rgba(0,0,0,0.05);background-color:#eaf1f1;text-align:center}.section-tout .icon{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;width:80px;height:80px;margin:0 auto 1rem;background:#349aed;background:linear-gradient(145deg, #3b9cea 50%, #3db8eb 100%);border-radius:50%;font-size:2rem;color:rgba(0,0,0,0.5)}.section-tout p{margin-bottom:5em}.section-preview{padding:4em 0}.section-preview .preview{margin-bottom:4em;background-color:#eaf1f1}.section-preview .preview .image{position:relative}.section-preview .preview .image::before{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.1);position:absolute;top:0;left:0;width:100%;height:100%;content:"";pointer-events:none}.section-preview .preview .options{padding:2em;border:1px solid rgba(0,0,0,0.05);border-top:none;text-align:center}.section-preview .preview .options p{margin-bottom:2em}.section-preview .dropdown-menu{text-align:left}.section-preview .lead{margin-bottom:2em}.sponsor #carbonads{max-width:240px;margin:0 auto}.sponsor .carbon-text{display:block;margin-top:1em;font-size:12px}.sponsor .carbon-poweredby{float:right;margin-top:1em;font-size:10px}@media (max-width: 767px){.splash{padding-top:8em}.splash .logo{width:100px}.splash h1{font-size:2em}#banner{margin-bottom:2em;text-align:center}}
|
||||
|
||||
/* CUSTOM STYLE */
|
||||
|
||||
/* Start collapsable table
|
||||
-------------------------------------------------- */
|
||||
|
||||
.hiddenRow, .hiddenCell {
|
||||
padding: 0px!important;
|
||||
border-top: 0px!important;
|
||||
}
|
||||
|
||||
.collapsedRow .col-md-6{
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.collapsedRow {
|
||||
padding: 10px 0px;
|
||||
border-top: 1px solid lightgray;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.collapse-indicator {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.collapse-indicator:after {
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
content: "\f056";
|
||||
}
|
||||
.collapse-indicator.collapsed:after {
|
||||
font-weight: 900;
|
||||
font-family: "Font Awesome 5 Free";
|
||||
content: "\f055";
|
||||
}
|
||||
|
||||
/* --------------------------------------------------
|
||||
End collapsable table*/
|
||||
|
||||
.jumbotron-home {
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.jumbotron-home {
|
||||
padding: 2rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.container, .container-lg, .container-md, .container-sm, .container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
.device-status-table {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand > img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.disabled-peer {
|
||||
color: #d03131;
|
||||
}
|
||||
|
||||
.expiring-peer {
|
||||
color: #d09d12;
|
||||
}
|
||||
|
||||
.tokenfield .token {
|
||||
border-radius: 0px;
|
||||
border: 1px solid #1a1a1a;
|
||||
color: #1a1a1a;
|
||||
background-color: #f7f7f9;
|
||||
margin: -4px 5px 5px 0;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.form-group.required label:after {
|
||||
content:"*";
|
||||
color:red;
|
||||
}
|
||||
|
||||
a.advanced-settings:before {
|
||||
content: "Hide";
|
||||
}
|
||||
|
||||
a.advanced-settings.collapsed:before {
|
||||
content: "Show";
|
||||
}
|
||||
|
||||
.form-group.global-config label:after, .custom-control.global-config label:after {
|
||||
content: "g";
|
||||
color: #0057bb;
|
||||
font-size: xx-small;
|
||||
top: -5px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.text-blue {
|
||||
color: #0057bb;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.pull-right-lg {
|
||||
float: right;
|
||||
}
|
||||
}
|
7
assets/css/jquery-ui.min.css
vendored
@ -1,8 +0,0 @@
|
||||
.navbar {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand > img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
5
assets/css/tokenfield-typeahead.min.css
vendored
@ -1,5 +0,0 @@
|
||||
/*!
|
||||
* bootstrap-tokenfield
|
||||
* https://github.com/sliptree/bootstrap-tokenfield
|
||||
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
||||
*/.twitter-typeahead{width:100%;position:relative;vertical-align:top}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{margin:0;width:100%;vertical-align:middle;background-color:#fff}.twitter-typeahead .tt-hint{color:#999;z-index:1;border:1px solid transparent}.twitter-typeahead .tt-input{color:#555;z-index:2}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429}.twitter-typeahead .input-sm.tt-input,.twitter-typeahead .hint-sm.tt-hint{border-radius:3px}.twitter-typeahead .input-lg.tt-input,.twitter-typeahead .hint-lg.tt-hint{border-radius:6px}.input-group .twitter-typeahead:first-child .tt-input,.input-group .twitter-typeahead:first-child .tt-hint{border-radius:4px 0 0 4px!important}.input-group .twitter-typeahead:last-child .tt-input,.input-group .twitter-typeahead:last-child .tt-hint{border-radius:0 4px 4px 0!important}.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint{border-radius:3px 0 0 3px!important}.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint{border-radius:0 3px 3px 0!important}.input-sm.tt-input,.hint-sm.tt-hint,.input-group.input-group-sm .tt-input,.input-group.input-group-sm .tt-hint{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint{border-radius:6px 0 0 6px!important}.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint{border-radius:0 6px 6px 0!important}.input-lg.tt-input,.hint-lg.tt-hint,.input-group.input-group-lg .tt-input,.input-group.input-group-lg .tt-hint{height:45px;padding:10px 16px;font-size:18px;line-height:1.33}.tt-dropdown-menu{width:100%;min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);*border-right-width:2px;*border-bottom-width:2px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-cursor{color:#262626;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.tt-suggestion.tt-cursor a{color:#fff}.tt-suggestion p{margin:0}.tokenfield .twitter-typeahead{width:auto}.tokenfield .twitter-typeahead .tt-hint{padding:0;height:20px}.tokenfield.input-sm .twitter-typeahead .tt-input,.tokenfield.input-sm .twitter-typeahead .tt-hint{height:18px;font-size:12px;line-height:1.5}.tokenfield.input-lg .twitter-typeahead .tt-input,.tokenfield.input-lg .twitter-typeahead .tt-hint{height:23px;font-size:18px;line-height:1.33}.tokenfield .twitter-typeahead .tt-suggestions{font-size:14px}
|
4
assets/fonts/font-awesome.min.css
vendored
13
assets/js/bootstrap-confirmation.min.js
vendored
7
assets/js/bootstrap-tokenfield.min.js
vendored
@ -1,39 +0,0 @@
|
||||
(function($) {
|
||||
"use strict"; // Start of use strict
|
||||
|
||||
// Smooth scrolling using jQuery easing
|
||||
$(document).on('click', 'a.scroll-to-top', function(e) {
|
||||
var $anchor = $(this);
|
||||
$('html, body').stop().animate({
|
||||
scrollTop: ($($anchor.attr('href')).offset().top)
|
||||
}, 1000, 'easeInOutExpo');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
|
||||
$(".online-status").each(function(){
|
||||
const onlineStatusID = "#" + $(this).attr('id');
|
||||
$.get( "/user/status?pkey=" + encodeURIComponent($(this).attr('data-pkey')), function( data ) {
|
||||
console.log(onlineStatusID + " " + data)
|
||||
if(data === true) {
|
||||
$(onlineStatusID).html('<i class="fas fa-link text-success"></i>');
|
||||
} else {
|
||||
$(onlineStatusID).html('<i class="fas fa-unlink"></i>');
|
||||
}
|
||||
});
|
||||
});
|
||||
$(function() {
|
||||
$('select.device-selector').change(function() {
|
||||
this.form.submit();
|
||||
});
|
||||
});
|
||||
$('[data-toggle=confirmation]').confirmation({
|
||||
rootSelector: '[data-toggle=confirmation]',
|
||||
// other options
|
||||
});
|
||||
})(jQuery); // End of use strict
|
||||
|
||||
|
13
assets/js/jquery-ui.min.js
vendored
@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/jquery-ui.min.css">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/bootstrap-tokenfield.min.css">
|
||||
<link rel="stylesheet" href="/css/tokenfield-typeahead.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>Create new clients</h1>
|
||||
<h2>Enter valid user email addresses to quickly create new accounts.</h2>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputEmail">Email Addresses</label>
|
||||
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputIdentifier">Client Friendly Name (will be added as suffix to the name of the user)</label>
|
||||
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/bootstrap-tokenfield.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
<script>$('#inputEmail').on('tokenfield:createdtoken', function (e) {
|
||||
// Über-simplistic e-mail validation
|
||||
var re = /\S+@\S+\.\S+/
|
||||
var valid = re.test(e.attrs.value)
|
||||
if (!valid) {
|
||||
$(e.relatedTarget).addClass('invalid')
|
||||
}
|
||||
}).on('tokenfield:createtoken', function (e) {
|
||||
var existingTokens = $(this).tokenfield('getTokens');
|
||||
$.each(existingTokens, function(index, token) {
|
||||
if (token.value === e.attrs.value)
|
||||
e.preventDefault();
|
||||
});
|
||||
}).tokenfield({
|
||||
autocomplete: {
|
||||
source: [{{range $i, $u :=.Users}}{{if ne $i 0}},{{end}}'{{$u.Email}}'{{end}}],
|
||||
delay: 100
|
||||
},
|
||||
inputType: 'email',
|
||||
createTokensOnBlur: true,
|
||||
showAutocompleteOnFocus: false
|
||||
})</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,221 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
{{if eq .Device.Type "server"}}
|
||||
{{if .Peer.IsNew}}
|
||||
<h1>Create a new client</h1>
|
||||
{{else}}
|
||||
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="endpoint" value="{{.Peer.Endpoint}}">
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Peer.PrivateKey}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PresharedKey">Preshared Key</label>
|
||||
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
|
||||
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Peer.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_Identifier">Client Friendly Name</label>
|
||||
<input type="text" name="identifier" class="form-control" id="server_Identifier" value="{{.Peer.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_Email">Client Email Address</label>
|
||||
<input type="email" name="mail" class="form-control" id="server_Email" value="{{.Peer.Email}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_IP">Client IP Address</label>
|
||||
<input type="text" name="ip" class="form-control" id="server_IP" value="{{.Peer.IPsStr}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 global-config">
|
||||
<label for="server_AllowedIP">Allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_AllowedIPSrv">Extra Allowed IPs (Server sided)</label>
|
||||
<input type="text" name="allowedipSrv" class="form-control" id="server_AllowedIPSrv" value="{{.Peer.AllowedIPsSrvStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 global-config">
|
||||
<label for="server_DNS">Client DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="server_DNS" value="{{.Peer.DNSStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6 global-config">
|
||||
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6 global-config">
|
||||
<label for="server_MTU">Client MTU (0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Peer.Mtu}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="ignoreglobalsettings" type="checkbox" value="true" id="server_IgnoreGlobalSettings" {{if .Peer.IgnoreGlobalSettings}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_IgnoreGlobalSettings">
|
||||
Ignore global settings (<span class="text-blue">g</span>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="expires_at">Expires At</label>
|
||||
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{{end}}
|
||||
|
||||
<!-- client mode -->
|
||||
{{if eq .Device.Type "client"}}
|
||||
{{if .Peer.IsNew}}
|
||||
<h1>Create a new remote endpoint</h1>
|
||||
{{else}}
|
||||
<h1>Edit remote endpoint: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<input type="hidden" name="mail" value="{{.AdminEmail}}">
|
||||
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_Identifier">Endpoint Friendly Name</label>
|
||||
<input type="text" name="identifier" class="form-control" id="client_Identifier" value="{{.Peer.Identifier}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_Endpoint">Endpoint Address</label>
|
||||
<input type="text" name="endpoint" class="form-control" id="client_Endpoint" value="{{.Peer.Endpoint}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PublicKey">Endpoint Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PresharedKey">Preshared Key</label>
|
||||
<input type="text" name="presharedkey" class="form-control" id="client_PresharedKey" value="{{.Peer.PresharedKey}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_AllowedIP">Allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="client_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_IP">Ping-Check IP Address</label>
|
||||
<input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="client_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="expires_at">Expires At</label>
|
||||
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,263 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5 main-app">
|
||||
<h1>Edit interface <strong>{{.Device.DeviceName}}</strong></h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{if eq .Device.Type "server"}}active{{end}}" data-toggle="tab" href="#server">Server Mode</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{if eq .Device.Type "client"}}active{{end}}" data-toggle="tab" href="#client">Client Mode</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id="configContent" class="tab-content">
|
||||
<!-- server mode -->
|
||||
<div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
|
||||
<form method="post" enctype="multipart/form-data" name="server">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="devicetype" value="server">
|
||||
<h3>Server's interface configuration</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_DisplayName">Display Name</label>
|
||||
<input type="text" name="displayname" class="form-control" id="server_DisplayName" value="{{.Device.DisplayName}}">
|
||||
</div>
|
||||
</div>
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Device.PrivateKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Device.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Device.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="server_ListenPort">Listen port</label>
|
||||
<input type="number" name="port" class="form-control" id="server_ListenPort" placeholder="51820" value="{{.Device.ListenPort}}" required>
|
||||
</div>
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="server_IPs">Server IP address</label>
|
||||
<input type="text" name="ip" class="form-control" id="server_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Client's global configuration (<span class="text-blue">g</span>)</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicEndpoint">Public Endpoint for Clients</label>
|
||||
<input type="text" name="endpoint" class="form-control" id="server_PublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.DefaultEndpoint}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_DNS">DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="server_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_AllowedIP">Default allowed IPs</label>
|
||||
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" placeholder="10.6.6.0/24" value="{{.Device.DefaultAllowedIPsStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_MTU">MTU (also used for the server interface, 0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Device.Mtu}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
|
||||
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Device.DefaultPersistentKeepalive}}">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Interface configuration hooks</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PreUp">Pre Up</label>
|
||||
<input type="text" name="preup" class="form-control" id="server_PreUp" value="{{.Device.PreUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PostUp">Post Up</label>
|
||||
<input type="text" name="postup" class="form-control" id="server_PostUp" value="{{.Device.PostUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PreDown">Pre Down</label>
|
||||
<input type="text" name="predown" class="form-control" id="server_PreDown" value="{{.Device.PreDown}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="server_PostDown">Post Down</label>
|
||||
<input type="text" name="postdown" class="form-control" id="server_PostDown" value="{{.Device.PostDown}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="#" class="advanced-settings btn btn-link collapsed" data-toggle="collapse" data-target="#collapseAdvancedServer" aria-expanded="false" aria-controls="collapseAdvancedServer">
|
||||
Advanced Settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collapseAdvancedServer" class="collapse" aria-labelledby="collapseAdvancedServer">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_FirewallMark">Firewall Mark (0 = default or off)</label>
|
||||
<input type="number" name="firewallmark" class="form-control" id="server_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="server_RoutingTable">Routing Table (empty = default or auto)</label>
|
||||
<input type="text" name="routingtable" class="form-control" id="server_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="saveconfig" type="checkbox" value="true" id="server_SaveConfig" {{if .Peer.SaveConfig}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_SaveConfig">
|
||||
Save Configuration (if interface was edited via WireGuard configuration tool)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Global Settings (<span class="text-blue">g</span>) to clients</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- client mode -->
|
||||
<div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
|
||||
<form method="post" enctype="multipart/form-data" name="client">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
|
||||
<input type="hidden" name="devicetype" value="client">
|
||||
<h3>Client's interface configuration</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_DisplayName">Display Name</label>
|
||||
<input type="text" name="displayname" class="form-control" id="client_DisplayName" value="{{.Device.DisplayName}}">
|
||||
</div>
|
||||
</div>
|
||||
{{if .EditableKeys}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PrivateKey">Private Key</label>
|
||||
<input type="text" name="privkey" class="form-control" id="client_PrivateKey" value="{{.Device.PrivateKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="client_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Device.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_ro_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" readonly class="form-control" id="client_ro_PublicKey" value="{{.Device.PublicKey}}">
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-6">
|
||||
<label for="client_IPs">Client IP address</label>
|
||||
<input type="text" name="ip" class="form-control" id="client_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="client_DNS">DNS Servers</label>
|
||||
<input type="text" name="dns" class="form-control" id="client_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_MTU">MTU (0 = default)</label>
|
||||
<input type="number" name="mtu" class="form-control" id="client_MTU" placeholder="" value="{{.Device.Mtu}}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_FirewallMark">Firewall Mark (0 = default or off)</label>
|
||||
<input type="number" name="firewallmark" class="form-control" id="client_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label for="client_RoutingTable">Routing Table (empty = default or auto)</label>
|
||||
<input type="text" name="routingtable" class="form-control" id="client_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
|
||||
</div>
|
||||
</div>
|
||||
<h3>Interface configuration hooks</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PreUp">Pre Up</label>
|
||||
<input type="text" name="preup" class="form-control" id="client_PreUp" value="{{.Device.PreUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PostUp">Post Up</label>
|
||||
<input type="text" name="postup" class="form-control" id="client_PostUp" value="{{.Device.PostUp}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PreDown">Pre Down</label>
|
||||
<input type="text" name="predown" class="form-control" id="client_PreDown" value="{{.Device.PreDown}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="client_PostDown">Post Down</label>
|
||||
<input type="text" name="postdown" class="form-control" id="client_PostDown" value="{{.Device.PostDown}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,95 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Users</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{if eq .User.CreatedAt .Epoch}}
|
||||
<h1>Create a new user</h1>
|
||||
{{else}}
|
||||
<h1>Edit user <strong>{{.User.Email}}</strong></h1>
|
||||
{{end}}
|
||||
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
{{if eq .User.CreatedAt .Epoch}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputEmail">Email</label>
|
||||
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input type="hidden" name="email" value="{{.User.Email}}">
|
||||
{{end}}
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputFirstname">Firstname</label>
|
||||
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="inputLastname">Lastname</label>
|
||||
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="inputPhone">Phone</label>
|
||||
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
|
||||
<label class="custom-control-label" for="inputAdmin">
|
||||
Administrator
|
||||
</label>
|
||||
</div>
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
|
||||
<label class="custom-control-label" for="inputDisabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
{{if eq .User.Source "db"}}
|
||||
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,286 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Administration</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
|
||||
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
|
||||
|
||||
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
|
||||
|
||||
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Endpoint:</td>
|
||||
<td>{{.Device.DefaultEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Listening Port:</td>
|
||||
<td>{{.Device.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Peers:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Peers:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default allowed IP's:</td>
|
||||
<td>{{.Device.DefaultAllowedIPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default Keepalive Interval:</td>
|
||||
<td>{{.Device.DefaultPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Endpoints:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Endpoints:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<h2 class="mt-2">Current VPN Peers</h2>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<h2 class="mt-2">Current VPN Endpoints</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
|
||||
{{end}}
|
||||
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
|
||||
{{end}}
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.Email}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<td>{{$p.Endpoint}}</td>
|
||||
{{end}}
|
||||
<td>
|
||||
<span
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title=""
|
||||
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
|
||||
{{$p.LastHandshakeRelativeTime}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
<a href="/admin/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="7" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Connection / Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-network-wired" title="Last Endpoint"></i> {{$p.Peer.Endpoint}}{{end}}</p>
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="t3{{$p.UID}}" class="tab-pane fade">
|
||||
<a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,69 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Users</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Users</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-10 col-12">
|
||||
<h2 class="mt-2">All Users</h2>
|
||||
</div>
|
||||
<div class="col-sm-2 col-12 text-right">
|
||||
<a href="/admin/users/create" title="Add a user" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><a href="?sort=email">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "users" "email"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=lastname">Lastname <i class="fa fa-fw {{.Session.GetSortIcon "users" "lastname"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=firstname">Firstname <i class="fa fa-fw {{.Session.GetSortIcon "users" "firstname"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=source">Source <i class="fa fa-fw {{.Session.GetSortIcon "users" "source"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=admin">Is Admin <i class="fa fa-fw {{.Session.GetSortIcon "users" "admin"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $u :=.Users}}
|
||||
<tr id="user-pos-{{$i}}" {{if $u.DeletedAt.Valid}}class="disabled-peer"{{end}}>
|
||||
<td>{{$u.Email}}</td>
|
||||
<td>{{$u.Lastname}}</td>
|
||||
<td>{{$u.Firstname}}</td>
|
||||
<td>{{$u.Source}}</td>
|
||||
<td>{{if $u.IsAdmin}}True{{else}}False{{end}}</td>
|
||||
<td>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
{{if eq $u.Source "db"}}
|
||||
<a href="/admin/users/edit?pkey={{$u.Email}}" title="Edit user"><i class="fas fa-cog"></i></a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed users: <strong>{{len .Users}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Error</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container">
|
||||
<div class="text-center mt-5">
|
||||
<div class="error mx-auto" data-text="{{.Data.Code}}">
|
||||
<p class="m-0">{{.Data.Code}}</p>
|
||||
</div>
|
||||
<p class="text-dark mb-5 lead">{{.Data.Message}}</p>
|
||||
<p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,89 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- Theme: https://bootswatch.com/lux/ -->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }}</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-2">
|
||||
<div class="page-header">
|
||||
<h1>{{ .Static.WebsiteTitle }}</h1>
|
||||
</div>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p>
|
||||
<h3 class="mt-3">More Information</h3>
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">WireGuard Installation</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">Installation</h4>
|
||||
<p class="card-text">Installation instructions for client software can be found on the official WireGuard website.</p>
|
||||
<a href="https://www.wireguard.com/install/" title="WireGuard Installation" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Instructions</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">About WireGuard</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">About</h4>
|
||||
<p class="card-text">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.</p>
|
||||
<a href="https://www.wireguard.com/" title="WireGuard" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-secondary mb-4" style="min-height: 15rem;">
|
||||
<div class="card-header">About WireGuard Portal</div>
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">WireGuard Portal</h4>
|
||||
<p class="card-text">WireGuard Portal is a simple, web based configuration portal for WireGuard.</p>
|
||||
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jumbotron jumbotron-home">
|
||||
<h2 class="display-5">VPN Profiles</h2>
|
||||
<p class="lead">You can access and download your personal VPN configurations via your Userprofile.</p>
|
||||
<hr class="my-4">
|
||||
<p>To find all your configured profiles click on the button below.</p>
|
||||
<p class="lead">
|
||||
<a href="/user/profile" class="btn btn-primary btn-lg" title="User-Profile">Open My Profile</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
<div class="jumbotron jumbotron-home">
|
||||
<h2 class="display-5">Administration Area</h2>
|
||||
<p class="lead">In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.</p>
|
||||
<hr class="my-4">
|
||||
<p>To find all your configured profiles click on the button below.</p>
|
||||
<p class="lead">
|
||||
<a href="/admin/" class="btn btn-primary btn-lg" title="WireGuard Administration">Open WireGuard Administration</a>
|
||||
<a href="/admin/users/" class="btn btn-primary btn-lg" title="User Administration">Open User Administration</a>
|
||||
</p>
|
||||
</div>
|
||||
{{end}}{{end}}
|
||||
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,66 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .static.WebsiteTitle }} - Login</title>
|
||||
<meta name="description" content="{{ .static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/fonts/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome5-overrides.min.css">
|
||||
<link rel="stylesheet" href="/css/signin.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.static.WebsiteLogo}}" alt="{{$.static.CompanyName}}"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
</div><!--/.navbar-collapse -->
|
||||
</nav>
|
||||
<div class="container mt-1">
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">Please sign in</div>
|
||||
<div class="card-body">
|
||||
<form class="form-signin" method="post" name="login">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<div class="form-group">
|
||||
<label for="inputUsername">Username</label>
|
||||
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter username or email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword">Password</label>
|
||||
<input type="password" name="password" class="form-control" id="inputPassword" placeholder="Password">
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-5" type="submit">Sign in</button>
|
||||
|
||||
{{ if eq .error true }}
|
||||
<div class="alert alert-danger mt-3" role="alert">
|
||||
{{.message}}
|
||||
</div>
|
||||
{{end}}
|
||||
</form>
|
||||
|
||||
<div class="card o-hidden border-0 my-5">
|
||||
<div class="card-body p-0">
|
||||
<a href="/" class="btn btn-white btn-block text-primary btn-user">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_flashes.html" .}}
|
||||
</div>
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,5 +0,0 @@
|
||||
{{range $flash := $.Alerts}}
|
||||
<div class="alert alert-{{$flash.Type}}" role="alert">
|
||||
{{$flash.Message}}
|
||||
</div>
|
||||
{{end}}
|
@ -1,5 +0,0 @@
|
||||
<footer class="page-footer mt-auto">
|
||||
<div class="container mt-3">
|
||||
<p class="text-muted">Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}}, version {{$.Static.Version}} <a class="float-right scroll-to-top" href="#page-top"><i class="fas fa-angle-up"></i></a></p>
|
||||
</div>
|
||||
</footer>
|
@ -1,61 +0,0 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.Static.WebsiteLogo}}" alt="{{$.Static.CompanyName}}"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
<li class="nav-spacer"></li>
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
{{with eq $.Route "/admin/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "peers"}}">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{with eq $.Route "/admin/users/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "users"}}">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}{{end}}
|
||||
</ul>
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
{{with startsWith $.Route "/admin/"}}
|
||||
<form class="form-inline my-2 my-lg-0" method="get">
|
||||
<div class="form-group mr-sm-2">
|
||||
<select name="device" id="inputDevice" class="form-control device-selector">
|
||||
{{range $d, $dn := $.DeviceNames}}
|
||||
<option value="{{$d}}" {{if eq $d $.Session.DeviceName}}selected{{end}}>{{$d}} {{if and (ne $dn "") (ne $d $dn)}}({{$dn}}){{end}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}{{end}}
|
||||
{{if eq $.Session.LoggedIn true}}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
|
||||
<div class="dropdown-menu">
|
||||
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
|
||||
<a class="dropdown-item" href="/admin/"><i class="fas fa-cogs"></i> Administration</a>
|
||||
<a class="dropdown-item" href="/admin/users/"><i class="fas fa-users-cog"></i> User Management</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
{{end}}{{end}}
|
||||
<a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/auth/logout"><i class="fas fa-sign-out-alt"></i> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<a href="/auth/login" class="navbar-text"><i class="fas fa-sign-in-alt fa-sm fa-fw mr-2 text-gray-400"></i> Login</a></li>
|
||||
{{end}}
|
||||
</div><!--/.navbar-collapse -->
|
||||
</nav>
|
||||
{{if not $.Device.IsValid}}
|
||||
<div class="container">
|
||||
<div class="alert alert-danger">Warning: WireGuard Interface {{$.Device.DeviceName}} is not fully configured! Configurations may be incomplete and non functional!</div>
|
||||
</div>
|
||||
{{end}}
|
@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
<h1>Create a new client</h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,54 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
{{template "prt_flashes.html" .}}
|
||||
|
||||
<!-- server mode -->
|
||||
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<input type="hidden" name="uid" value="{{.Peer.UID}}">
|
||||
<div class="form-row">
|
||||
<div class="form-group required col-md-12">
|
||||
<label for="server_PublicKey">Public Key</label>
|
||||
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required disabled="disabled">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}disabled="disabled"{{end}} {{if .Peer.DeactivatedAt}}checked{{end}}>
|
||||
<label class="custom-control-label" for="server_Disabled">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,145 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Profile</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN User-Portal</h1>
|
||||
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
<h2 class="mt-2">Your VPN Profiles</h2>
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<a href="/user/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "pubKey"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=device">Interface <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "device"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<th scope="col"></th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
<td>{{$p.Email}}</td>
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
<td>{{$p.DeviceName}}</td>
|
||||
<td>
|
||||
<span
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title=""
|
||||
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
|
||||
{{$p.LastHandshakeRelativeTime}}
|
||||
</span>
|
||||
</td>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<td>
|
||||
<a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="6" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p>{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down"></i></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
117
cmd/api_build_tool/main.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/swaggo/swag"
|
||||
"github.com/swaggo/swag/gen"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var apiRootPath = "/internal/app/api"
|
||||
var apiDocPath = "core/assets/doc"
|
||||
var apiMkDocPath = "/docs/documentation/rest-api"
|
||||
|
||||
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
|
||||
func main() {
|
||||
wd, err := os.Getwd() // should be the project root
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
apiBasePath := filepath.Join(wd, apiRootPath)
|
||||
apis := []string{"v0", "v1"}
|
||||
|
||||
for _, apiVersion := range apis {
|
||||
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
|
||||
|
||||
apiVersion = strings.TrimLeft(apiVersion, "api-")
|
||||
log.Println("")
|
||||
log.Println("Generate swagger docs for API", apiVersion)
|
||||
log.Println("Api path:", apiPath)
|
||||
|
||||
err := generateApi(apiBasePath, apiPath, apiVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
}
|
||||
|
||||
// copy the latest version of the API docs for mkdocs
|
||||
if apiVersion == apis[len(apis)-1] {
|
||||
if err = copyDocForMkdocs(wd, apiBasePath, apiVersion); err != nil {
|
||||
log.Printf("failed to copy API docs for mkdocs: %v", err)
|
||||
} else {
|
||||
log.Println("Copied API docs " + apiVersion + " for mkdocs")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Generated swagger docs for API", apiVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func generateApi(basePath, apiPath, version string) error {
|
||||
err := gen.New().Build(&gen.Config{
|
||||
SearchDir: apiPath,
|
||||
Excludes: "",
|
||||
MainAPIFile: "base.go",
|
||||
PropNamingStrategy: swag.PascalCase,
|
||||
OutputDir: filepath.Join(basePath, apiDocPath),
|
||||
OutputTypes: []string{"json", "yaml"},
|
||||
ParseVendor: false,
|
||||
ParseDependency: 3,
|
||||
MarkdownFilesDir: "",
|
||||
ParseInternal: true,
|
||||
GeneratedTime: false,
|
||||
CodeExampleFilesDir: "",
|
||||
ParseDepth: 3,
|
||||
InstanceName: version,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("swag failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDocForMkdocs(workingDir, basePath, version string) error {
|
||||
srcPath := filepath.Join(basePath, apiDocPath, fmt.Sprintf("%s_swagger.yaml", version))
|
||||
dstPath := filepath.Join(workingDir, apiMkDocPath, "swagger.yaml")
|
||||
|
||||
// copy the file
|
||||
input, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while reading swagger doc: %w", err)
|
||||
}
|
||||
|
||||
output, err := removeAuthorizeButton(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while removing authorize button: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(dstPath, output, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while writing swagger doc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
||||
var swagger map[string]any
|
||||
err := yaml.Unmarshal(input, &swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
delete(swagger, "securityDefinitions")
|
||||
|
||||
output, err := yaml.Marshal(&swagger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while marshalling swagger file: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// main checks the given URL, if the response is not 200, it will return with exit code 1
|
||||
// on success, exit code 0 will be returned
|
||||
func main() {
|
||||
os.Exit(checkWebEndpointFromArgs())
|
||||
}
|
||||
|
||||
func checkWebEndpointFromArgs() int {
|
||||
if len(os.Args) < 2 {
|
||||
return 1
|
||||
}
|
||||
if status := checkWebEndpoint(os.Args[1]); !status {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func checkWebEndpoint(url string) bool {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 2,
|
||||
}
|
||||
if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@ -2,103 +2,188 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/common/healthcheck"
|
||||
"github.com/h44z/wg-portal/internal/server"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/go-playground/validator/v10"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/adapters"
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||
backendV0 "github.com/h44z/wg-portal/internal/app/api/v0/backend"
|
||||
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
||||
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
||||
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||
"github.com/h44z/wg-portal/internal/app/audit"
|
||||
"github.com/h44z/wg-portal/internal/app/auth"
|
||||
"github.com/h44z/wg-portal/internal/app/configfile"
|
||||
"github.com/h44z/wg-portal/internal/app/mail"
|
||||
"github.com/h44z/wg-portal/internal/app/route"
|
||||
"github.com/h44z/wg-portal/internal/app/users"
|
||||
"github.com/h44z/wg-portal/internal/app/webhooks"
|
||||
"github.com/h44z/wg-portal/internal/app/wireguard"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
)
|
||||
|
||||
// main entry point for WireGuard Portal
|
||||
func main() {
|
||||
_ = setupLogger(logrus.StandardLogger())
|
||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
slog.Info("Starting WireGuard Portal V2...", "version", internal.Version)
|
||||
|
||||
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
|
||||
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
|
||||
cfg, err := config.GetConfig()
|
||||
internal.AssertNoError(err)
|
||||
internal.SetupLogging(cfg.Advanced.LogLevel, cfg.Advanced.LogPretty, cfg.Advanced.LogJson)
|
||||
|
||||
// Context for clean shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
cfg.LogStartupValues()
|
||||
|
||||
// start health check service on port 11223
|
||||
healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
|
||||
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
|
||||
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
service := server.Server{}
|
||||
if err := service.Setup(ctx); err != nil {
|
||||
logrus.Fatalf("setup failed: %v", err)
|
||||
database, err := adapters.NewSqlRepository(rawDb)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
wireGuard := adapters.NewWireGuardRepository()
|
||||
|
||||
wgQuick := adapters.NewWgQuickRepo()
|
||||
|
||||
mailer := adapters.NewSmtpMailRepo(cfg.Mail)
|
||||
|
||||
metricsServer := adapters.NewMetricsServer(cfg)
|
||||
|
||||
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
shouldExit, err := app.HandleProgramArgs(rawDb)
|
||||
switch {
|
||||
case shouldExit && err == nil:
|
||||
return
|
||||
case shouldExit:
|
||||
slog.Error("Failed to process program args", "error", err)
|
||||
os.Exit(1)
|
||||
default:
|
||||
internal.AssertNoError(err)
|
||||
}
|
||||
|
||||
// Attach signal handlers to context
|
||||
go func() {
|
||||
osCall := <-c
|
||||
logrus.Tracef("received system call: %v", osCall)
|
||||
cancel() // cancel the context
|
||||
}()
|
||||
queueSize := 100
|
||||
eventBus := evbus.New(queueSize)
|
||||
|
||||
// Start main process in background
|
||||
go service.Run()
|
||||
auditManager := audit.NewManager(database)
|
||||
|
||||
<-ctx.Done() // Wait until the context gets canceled
|
||||
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
||||
internal.AssertNoError(err)
|
||||
auditRecorder.StartBackgroundJobs(ctx)
|
||||
|
||||
// Give goroutines some time to stop gracefully
|
||||
logrus.Info("stopping WireGuard Portal Server...")
|
||||
time.Sleep(2 * time.Second)
|
||||
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
||||
internal.AssertNoError(err)
|
||||
userManager.StartBackgroundJobs(ctx)
|
||||
|
||||
logrus.Infof("stopped WireGuard Portal Server...")
|
||||
logrus.Exit(0)
|
||||
}
|
||||
|
||||
func setupLogger(logger *logrus.Logger) error {
|
||||
// Check environment variables for logrus settings
|
||||
level, ok := os.LookupEnv("LOG_LEVEL")
|
||||
if !ok {
|
||||
level = "debug" // Default logrus level
|
||||
}
|
||||
|
||||
useJSON, ok := os.LookupEnv("LOG_JSON")
|
||||
if !ok {
|
||||
useJSON = "false" // Default use human readable logging
|
||||
}
|
||||
|
||||
useColor, ok := os.LookupEnv("LOG_COLOR")
|
||||
if !ok {
|
||||
useColor = "true"
|
||||
}
|
||||
|
||||
switch level {
|
||||
case "off":
|
||||
logger.SetOutput(io.Discard)
|
||||
case "info":
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
case "debug":
|
||||
logger.SetLevel(logrus.DebugLevel)
|
||||
case "trace":
|
||||
logger.SetLevel(logrus.TraceLevel)
|
||||
}
|
||||
|
||||
var formatter logrus.Formatter
|
||||
if useJSON == "false" {
|
||||
f := new(logrus.TextFormatter)
|
||||
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||
f.FullTimestamp = true
|
||||
if useColor == "true" {
|
||||
f.ForceColors = true
|
||||
}
|
||||
formatter = f
|
||||
} else {
|
||||
f := new(logrus.JSONFormatter)
|
||||
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||
formatter = f
|
||||
}
|
||||
|
||||
logger.SetFormatter(formatter)
|
||||
|
||||
return nil
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
||||
internal.AssertNoError(err)
|
||||
wireGuardManager.StartBackgroundJobs(ctx)
|
||||
|
||||
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
|
||||
internal.AssertNoError(err)
|
||||
statisticsCollector.StartBackgroundJobs(ctx)
|
||||
|
||||
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
||||
internal.AssertNoError(err)
|
||||
routeManager.StartBackgroundJobs(ctx)
|
||||
|
||||
webhookManager, err := webhooks.NewManager(cfg, eventBus)
|
||||
internal.AssertNoError(err)
|
||||
webhookManager.StartBackgroundJobs(ctx)
|
||||
|
||||
err = app.Initialize(cfg, wireGuardManager, userManager)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
validatorManager := validator.New()
|
||||
|
||||
// region API v0 (SPA frontend)
|
||||
|
||||
apiV0Session := handlersV0.NewSessionWrapper(cfg)
|
||||
apiV0Auth := handlersV0.NewAuthenticationHandler(authenticator, apiV0Session)
|
||||
|
||||
apiV0BackendUsers := backendV0.NewUserService(cfg, userManager, wireGuardManager)
|
||||
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
|
||||
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
|
||||
|
||||
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator)
|
||||
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
|
||||
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
||||
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
||||
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth)
|
||||
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||
|
||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||
apiV0EndpointAuth,
|
||||
apiV0EndpointAudit,
|
||||
apiV0EndpointUsers,
|
||||
apiV0EndpointInterfaces,
|
||||
apiV0EndpointPeers,
|
||||
apiV0EndpointConfig,
|
||||
apiV0EndpointTest,
|
||||
)
|
||||
|
||||
// endregion API v0 (SPA frontend)
|
||||
|
||||
// region API v1 (User REST API)
|
||||
|
||||
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
|
||||
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
||||
|
||||
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1Auth, validatorManager, apiV1BackendUsers)
|
||||
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1Auth, validatorManager, apiV1BackendPeers)
|
||||
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1Auth, validatorManager, apiV1BackendInterfaces)
|
||||
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1Auth, validatorManager,
|
||||
apiV1BackendProvisioning)
|
||||
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1Auth, validatorManager, apiV1BackendMetrics)
|
||||
|
||||
apiV1 := handlersV1.NewRestApi(
|
||||
apiV1EndpointUsers,
|
||||
apiV1EndpointPeers,
|
||||
apiV1EndpointInterfaces,
|
||||
apiV1EndpointProvisioning,
|
||||
apiV1EndpointMetrics,
|
||||
)
|
||||
|
||||
// endregion API v1 (User REST API)
|
||||
|
||||
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
go metricsServer.Run(ctx)
|
||||
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
||||
|
||||
slog.Info("Application startup complete")
|
||||
|
||||
// wait until context gets cancelled
|
||||
<-ctx.Done()
|
||||
|
||||
slog.Info("Stopping WireGuard Portal")
|
||||
|
||||
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
||||
|
||||
slog.Info("Stopped WireGuard Portal")
|
||||
}
|
||||
|
96
config.yml.sample
Normal file
@ -0,0 +1,96 @@
|
||||
# More information about the configuration can be found in the documentation: https://wgportal.org/master/documentation/overview/
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
|
||||
core:
|
||||
admin_user: test@test.de
|
||||
admin_password: secret
|
||||
create_default_peer: true
|
||||
create_default_peer_on_creation: false
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
request_logging: true
|
||||
|
||||
webhook:
|
||||
url: ""
|
||||
authentication: ""
|
||||
timeout: 10s
|
||||
|
||||
auth:
|
||||
ldap:
|
||||
- id: ldap1
|
||||
provider_name: company ldap
|
||||
url: ldap://ldap.yourcompany.local:389
|
||||
bind_user: ldap_wireguard@yourcompany.local
|
||||
bind_pass: super_Secret_PASSWORD
|
||||
base_dn: DC=YOURCOMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
admin_group: CN=WireGuardAdmins,OU=it,DC=YOURCOMPANY,DC=LOCAL
|
||||
sync_interval: 0 # sync disabled
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
registration_enabled: true
|
||||
oidc:
|
||||
- id: oidc1
|
||||
provider_name: google
|
||||
display_name: Login with</br>Google
|
||||
base_url: https://accounts.google.com
|
||||
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
registration_enabled: true
|
||||
- id: oidc2
|
||||
provider_name: google2
|
||||
display_name: Login with</br>Google2
|
||||
base_url: https://accounts.google.com
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
registration_enabled: true
|
||||
oauth:
|
||||
- id: google_plain_oauth
|
||||
provider_name: google3
|
||||
display_name: Login with</br>Google3
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: this-attribute-must-be-true
|
||||
registration_enabled: true
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
5
ct.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# See https://github.com/helm/chart-testing#configuration
|
||||
remote: origin
|
||||
chart-dirs: deploy
|
||||
target-branch: master
|
||||
validate-maintainers: false
|
23
deploy/helm/.helmignore
Normal file
@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
25
deploy/helm/Chart.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
apiVersion: v2
|
||||
name: wg-portal
|
||||
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
# Version is set to ensure compatibility with the chart's Ingress resource.
|
||||
kubeVersion: ">=1.19.0"
|
||||
type: application
|
||||
home: https://wgportal.org
|
||||
icon: https://wgportal.org/latest/assets/images/logo.svg
|
||||
sources:
|
||||
- https://github.com/h44z/wg-portal
|
||||
|
||||
annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: ""
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.7.1
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v2"
|
124
deploy/helm/README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# wg-portal
|
||||
|
||||
  
|
||||
|
||||
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
|
||||
**Homepage:** <https://wgportal.org>
|
||||
|
||||
## Source Code
|
||||
|
||||
* <https://github.com/h44z/wg-portal>
|
||||
|
||||
## Requirements
|
||||
|
||||
Kubernetes: `>=1.19.0`
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `wg-portal`:
|
||||
|
||||
```console
|
||||
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
|
||||
```
|
||||
|
||||
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
|
||||
The [Values](#values) section lists the parameters that can be configured during installation.
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
||||
| fullnameOverride | string | `""` | Fully override resource names |
|
||||
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
||||
| config.advanced | tpl/object | `{}` | [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options. |
|
||||
| config.auth | tpl/object | `{}` | [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options. |
|
||||
| config.core | tpl/object | `{}` | [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br> If external admins in `auth` are defined and there are no `admin_user` and `admin_password` defined here, the default admin account will be disabled. |
|
||||
| config.database | tpl/object | `{}` | [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options |
|
||||
| config.mail | tpl/object | `{}` | [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options |
|
||||
| config.statistics | tpl/object | `{}` | [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options |
|
||||
| config.web | tpl/object | `{}` | [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br> `listening_address` will be set automatically from `service.web.port`. `external_url` is required to enable ingress and certificate resources. |
|
||||
| revisionHistoryLimit | string | `10` | The number of old ReplicaSets to retain to allow rollback. |
|
||||
| workloadType | string | `"Deployment"` | Workload type - `Deployment` or `StatefulSet` |
|
||||
| strategy | object | `{"type":"RollingUpdate"}` | Update strategy for the workload Valid values are: `RollingUpdate` or `Recreate` for Deployment, `RollingUpdate` or `OnDelete` for StatefulSet |
|
||||
| image.repository | string | `"ghcr.io/h44z/wg-portal"` | Image repository |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
|
||||
| image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion |
|
||||
| imagePullSecrets | list | `[]` | Image pull secrets |
|
||||
| podAnnotations | tpl/object | `{}` | Extra annotations to add to the pod |
|
||||
| podLabels | object | `{}` | Extra labels to add to the pod |
|
||||
| podSecurityContext | object | `{}` | Pod Security Context |
|
||||
| securityContext.capabilities.add | list | `["NET_ADMIN"]` | Add capabilities to the container |
|
||||
| initContainers | tpl/list | `[]` | Pod init containers |
|
||||
| sidecarContainers | tpl/list | `[]` | Pod sidecar containers |
|
||||
| dnsPolicy | string | `"ClusterFirst"` | Set DNS policy for the pod. Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`. |
|
||||
| restartPolicy | string | `"Always"` | Restart policy for all containers within the pod. Valid values are `Always`, `OnFailure` or `Never`. |
|
||||
| hostNetwork | string | `false`. | Use the host's network namespace. |
|
||||
| resources | object | `{}` | Resources requests and limits |
|
||||
| command | list | `[]` | Overwrite pod command |
|
||||
| args | list | `[]` | Additional pod arguments |
|
||||
| env | tpl/list | `[]` | Additional environment variables |
|
||||
| envFrom | tpl/list | `[]` | Additional environment variables from a secret or configMap |
|
||||
| livenessProbe | object | `{}` | Liveness probe configuration |
|
||||
| readinessProbe | object | `{}` | Readiness probe configuration |
|
||||
| startupProbe | object | `{}` | Startup probe configuration |
|
||||
| volumes | tpl/list | `[]` | Additional volumes |
|
||||
| volumeMounts | tpl/list | `[]` | Additional volumeMounts |
|
||||
| nodeSelector | object | `{"kubernetes.io/os":"linux"}` | Node Selector configuration |
|
||||
| tolerations | list | `[]` | Tolerations configuration |
|
||||
| affinity | object | `{}` | Affinity configuration |
|
||||
| service.mixed.enabled | bool | `false` | Whether to create a single service for the web and wireguard interfaces |
|
||||
| service.mixed.type | string | `"LoadBalancer"` | Service type |
|
||||
| service.web.annotations | object | `{}` | Annotations for the web service |
|
||||
| service.web.type | string | `"ClusterIP"` | Web service type |
|
||||
| service.web.port | int | `8888` | Web service port Used for the web interface listener |
|
||||
| service.web.appProtocol | string | `"http"` | Web service appProtocol. Will be auto set to `https` if certificate is enabled. |
|
||||
| service.wireguard.annotations | object | `{}` | Annotations for the WireGuard service |
|
||||
| service.wireguard.type | string | `"LoadBalancer"` | Wireguard service type |
|
||||
| service.wireguard.ports | list | `[51820]` | Wireguard service ports. Exposes the WireGuard ports for created interfaces. Lowerest port is selected as start port for the first interface. Increment next port by 1 for each additional interface. |
|
||||
| service.metrics.port | int | `8787` | |
|
||||
| ingress.enabled | bool | `false` | Specifies whether an ingress resource should be created |
|
||||
| ingress.className | string | `""` | Ingress class name |
|
||||
| ingress.annotations | object | `{}` | Ingress annotations |
|
||||
| ingress.tls | bool | `false` | Ingress TLS configuration. Enable certificate resource or add ingress annotation to create required secret |
|
||||
| certificate.enabled | bool | `false` | Specifies whether a certificate resource should be created. If enabled, certificate will be used for the web. |
|
||||
| certificate.issuer.name | string | `""` | Certificate issuer name |
|
||||
| certificate.issuer.kind | string | `""` | Certificate issuer kind (ClusterIssuer or Issuer) |
|
||||
| certificate.issuer.group | string | `"cert-manager.io"` | Certificate issuer group |
|
||||
| certificate.duration | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.renewBefore | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.commonName | string | `""` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.emailAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.ipAddresses | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.keystores | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.privateKey | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.secretTemplate | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.subject | object | `{}` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.uris | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| certificate.usages | list | `[]` | Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources) |
|
||||
| persistence.enabled | bool | `false` | Specifies whether an persistent volume should be created |
|
||||
| persistence.annotations | object | `{}` | Persistent Volume Claim annotations |
|
||||
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
||||
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
||||
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||
| persistence.volumeName | string | `""` | Persistent Volume Name (optional) |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
||||
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| monitoring.enabled | bool | `false` | Enable Prometheus monitoring. |
|
||||
| monitoring.apiVersion | string | `"monitoring.coreos.com/v1"` | API version of the Prometheus resource. Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus. |
|
||||
| monitoring.kind | string | `"PodMonitor"` | Kind of the Prometheus resource. Could be `PodMonitor` or `ServiceMonitor`. |
|
||||
| monitoring.labels | object | `{}` | Resource labels. |
|
||||
| monitoring.annotations | object | `{}` | Resource annotations. |
|
||||
| monitoring.interval | string | `1m` | Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used. |
|
||||
| monitoring.metricRelabelings | list | `[]` | Relabelings to samples before ingestion. |
|
||||
| monitoring.relabelings | list | `[]` | Relabelings to samples before scraping. |
|
||||
| monitoring.scrapeTimeout | string | `""` | Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used. |
|
||||
| monitoring.jobLabel | string | `""` | The label to use to retrieve the job name from. |
|
||||
| monitoring.podTargetLabels | object | `{}` | Transfers labels on the Kubernetes Pod onto the target. |
|
||||
| monitoring.dashboard.enabled | bool | `false` | Enable Grafana dashboard. |
|
||||
| monitoring.dashboard.annotations | object | `{}` | Annotations for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.labels | object | `{}` | Additional labels for the dashboard ConfigMap. |
|
||||
| monitoring.dashboard.namespace | string | `""` | Dashboard ConfigMap namespace Overrides the namespace for the dashboard ConfigMap. |
|
27
deploy/helm/README.md.gotmpl
Normal file
@ -0,0 +1,27 @@
|
||||
{{ template "chart.header" . }}
|
||||
{{ template "chart.deprecationWarning" . }}
|
||||
|
||||
{{ template "chart.badgesSection" . }}
|
||||
|
||||
{{ template "chart.description" . }}
|
||||
|
||||
{{ template "chart.homepageLine" . }}
|
||||
|
||||
{{ template "chart.maintainersSection" . }}
|
||||
|
||||
{{ template "chart.sourcesSection" . }}
|
||||
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `wg-portal`:
|
||||
|
||||
```console
|
||||
helm install wg-portal oci://ghcr.io/h44z/charts/wg-portal
|
||||
```
|
||||
|
||||
This command deploy wg-portal on the Kubernetes cluster in the default configuration.
|
||||
The [Values](#values) section lists the parameters that can be configured during installation.
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
917
deploy/helm/files/dashboard.json
Normal file
@ -0,0 +1,917 @@
|
||||
{
|
||||
"annotations": {},
|
||||
"description": "WireGuard Portal Dashboard",
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 3600000,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum by (instance, interface) (wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
|
||||
"fullMetaSearch": false,
|
||||
"hide": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "Received {{interface}}",
|
||||
"range": true,
|
||||
"refId": "A",
|
||||
"useBackend": false
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (instance, interface) (wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"})",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"legendFormat": "Sent {{interface}}",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Interface Bytes Total",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 3600000,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (instance, interface) (rate(wireguard_interface_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "Received {{interface}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (instance, interface) (rate(wireguard_interface_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "Sent {{interface}}",
|
||||
"range": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Interface Bandwidth",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 3600000,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 16,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (name, instance, interface) (rate(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "$interval",
|
||||
"legendFormat": "{{name}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Peer Receive Bandwidth",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "opacity",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": 3600000,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 17,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "right",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"expr": "sum by (instance, interface, name) (rate(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__rate_interval]))",
|
||||
"hide": false,
|
||||
"instant": false,
|
||||
"interval": "$interval",
|
||||
"legendFormat": "{{name}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Peer Transmit Bandwidth",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"fillOpacity": 60,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "bool_yes_no"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"colWidth": 0.85,
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": false
|
||||
},
|
||||
"rowHeight": 0.85,
|
||||
"showValue": "never",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum by(name) (wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"})",
|
||||
"instant": false,
|
||||
"interval": "$interval",
|
||||
"legendFormat": "{{name}}",
|
||||
"range": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Peer Connection History",
|
||||
"type": "status-history"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"default": false,
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic-by-name"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto",
|
||||
"wrapText": false
|
||||
},
|
||||
"filterable": false,
|
||||
"inspect": false
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "dark-red",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/(Time|instance|interface|name)\\s\\d*/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hidden",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byRegexp",
|
||||
"options": "/Received|Transmitted/"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "bytes"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Last Handshake"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "s"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Connected"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "mappings",
|
||||
"value": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"color": "red",
|
||||
"index": 0,
|
||||
"text": "No"
|
||||
},
|
||||
"1": {
|
||||
"color": "green",
|
||||
"index": 1,
|
||||
"text": "Yes"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "custom.cellOptions",
|
||||
"value": {
|
||||
"type": "color-text"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 14,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 29
|
||||
},
|
||||
"id": 11,
|
||||
"options": {
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"enablePagination": false,
|
||||
"fields": [],
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": [
|
||||
{
|
||||
"desc": true,
|
||||
"displayName": "Sent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum by(id, instance, interface, name, addresses) (increase(wireguard_peer_received_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
|
||||
"format": "table",
|
||||
"fullMetaSearch": false,
|
||||
"hide": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A",
|
||||
"useBackend": false
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum by(id, instance, interface, name) (increase(wireguard_peer_sent_bytes_total{instance=\"$instance\", interface=~\"$interface\"}[$__range]))",
|
||||
"format": "table",
|
||||
"fullMetaSearch": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "B",
|
||||
"useBackend": false
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "time()-sum(wireguard_peer_last_handshake_seconds{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "__auto",
|
||||
"range": false,
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "sum(wireguard_peer_up{instance=\"$instance\", interface=~\"$interface\"}) by(id, instance, interface, name) ",
|
||||
"format": "table",
|
||||
"hide": false,
|
||||
"instant": true,
|
||||
"interval": "",
|
||||
"legendFormat": "__auto",
|
||||
"range": false,
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"title": "Peer Info",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "joinByField",
|
||||
"options": {
|
||||
"byField": "id",
|
||||
"mode": "outer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time 1": false,
|
||||
"Time 2": false,
|
||||
"Time 3": false,
|
||||
"Time 4": false
|
||||
},
|
||||
"includeByName": {},
|
||||
"indexByName": {
|
||||
"Time 1": 8,
|
||||
"Time 2": 9,
|
||||
"Time 3": 10,
|
||||
"Time 4": 11,
|
||||
"Value #A": 4,
|
||||
"Value #B": 5,
|
||||
"Value #C": 6,
|
||||
"Value #D": 7,
|
||||
"addresses": 2,
|
||||
"id": 3,
|
||||
"instance 1": 12,
|
||||
"instance 2": 13,
|
||||
"instance 3": 16,
|
||||
"instance 4": 19,
|
||||
"interface 1": 0,
|
||||
"interface 2": 14,
|
||||
"interface 3": 17,
|
||||
"interface 4": 20,
|
||||
"name 1": 1,
|
||||
"name 2": 15,
|
||||
"name 3": 18,
|
||||
"name 4": 21
|
||||
},
|
||||
"renameByName": {
|
||||
"Value #A": "Received",
|
||||
"Value #B": "Transmitted",
|
||||
"Value #C": "Last Handshake",
|
||||
"Value #D": "Connected",
|
||||
"addresses": "IP Addresses",
|
||||
"id": "Public Key",
|
||||
"interface": "Interface",
|
||||
"interface 1": "Interface",
|
||||
"name": "Name",
|
||||
"name 1": "Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": "1m",
|
||||
"tags": [
|
||||
"wireguard",
|
||||
"vpn"
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Prometheus",
|
||||
"multi": false,
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"definition": "label_values(wireguard_interface_sent_bytes_total,instance)",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Instance",
|
||||
"multi": false,
|
||||
"name": "instance",
|
||||
"options": [],
|
||||
"query": {
|
||||
"qryType": 1,
|
||||
"query": "label_values(wireguard_interface_sent_bytes_total,instance)",
|
||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${datasource}"
|
||||
},
|
||||
"definition": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Interface",
|
||||
"multi": true,
|
||||
"name": "interface",
|
||||
"options": [],
|
||||
"query": {
|
||||
"qryType": 1,
|
||||
"query": "label_values(wireguard_interface_sent_bytes_total{instance=\"$instance\"},interface)",
|
||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"text": "2m",
|
||||
"value": "2m"
|
||||
},
|
||||
"description": "",
|
||||
"label": "Step Interval",
|
||||
"name": "interval",
|
||||
"options": [
|
||||
{
|
||||
"selected": false,
|
||||
"text": "30s",
|
||||
"value": "30s"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "1m",
|
||||
"value": "1m"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "2m",
|
||||
"value": "2m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "5m",
|
||||
"value": "5m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "10m",
|
||||
"value": "10m"
|
||||
}
|
||||
],
|
||||
"query": "30s,1m,2m,5m,10m",
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-12h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "WireGuard Portal",
|
||||
"uid": "wireguard-portal",
|
||||
"weekStart": ""
|
||||
}
|
24
deploy/helm/templates/NOTES.txt
Normal file
@ -0,0 +1,24 @@
|
||||
{{- $serviceName := printf "%s-web" (include "wg-portal.fullname" .) -}}
|
||||
{{- $servicePort := .Values.service.web.port }}
|
||||
|
||||
{{- if not .Values.ingress.enabled }}
|
||||
Get the application URL by running these commands:
|
||||
{{- if eq "ClusterIP" .Values.service.web.type }}
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ $serviceName }} {{ $servicePort }}:{{ $servicePort }}
|
||||
|
||||
Visit http://127.0.0.1:{{ $servicePort }} to use your application
|
||||
|
||||
{{- else if eq "LoadBalancer" .Values.service.web.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ $serviceName }}'
|
||||
export SERVICE_IP=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ $servicePort }}
|
||||
|
||||
{{- else if eq "NodePort" .Values.service.web.type }}
|
||||
export NODE_IP=$(kubectl get --namespace {{ .Release.Namespace }} nodes -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} svc {{ $serviceName }} -o jsonpath="{.spec.ports[0].nodePort}" )
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
Visit http{{ if .Values.ingress.tls }}s{{ end }}://{{ .Values.ingress.host }}{{ .Values.ingress.path }} to use your application
|
||||
{{- end }}
|
132
deploy/helm/templates/_helpers.tpl
Normal file
@ -0,0 +1,132 @@
|
||||
{{/*
|
||||
Expand the name of the chart
|
||||
*/}}
|
||||
{{- define "wg-portal.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "wg-portal.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label
|
||||
*/}}
|
||||
{{- define "wg-portal.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "wg-portal.labels" -}}
|
||||
helm.sh/chart: {{ include "wg-portal.chart" . }}
|
||||
{{ include "wg-portal.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "wg-portal.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "wg-portal.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "wg-portal.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "wg-portal.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Disables default admin credentials
|
||||
If external auth is enabled and has admin group mappings,
|
||||
the admin_user will be set to blank (disabled).
|
||||
*/}}
|
||||
{{- define "wg-portal.admin" -}}
|
||||
{{- $externalAdmin := false -}}
|
||||
{{- with .Values.config.auth -}}
|
||||
{{- range (default list .ldap) -}}
|
||||
{{- if hasKey . "admin_group" -}}
|
||||
{{- $externalAdmin = true -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- range (concat (default list .oidc) (default list .oauth)) -}}
|
||||
{{- if hasKey .field_map "is_admin" -}}
|
||||
{{- $externalAdmin = true -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if $externalAdmin -}}
|
||||
admin_user: ""
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define PersistentVolumeClaim spec
|
||||
*/}}
|
||||
{{- define "wg-portal.pvc" -}}
|
||||
accessModes:
|
||||
- {{ .Values.persistence.accessMode }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size | quote }}
|
||||
{{- with .Values.persistence.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.persistence.volumeName }}
|
||||
volumeName: {{ . }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define hostname
|
||||
*/}}
|
||||
{{- define "wg-portal.hostname" -}}
|
||||
{{- if .Values.config.web.external_url -}}
|
||||
{{- (urlParse (tpl .Values.config.web.external_url .)).hostname -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{/*
|
||||
wg-portal.util.merge will merge two YAML templates or dict with template and output the result.
|
||||
This takes an array of three values:
|
||||
- the top context
|
||||
- the template name or dict of the overrides (destination)
|
||||
- the template name of the base (source)
|
||||
{{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") }}
|
||||
{{- include "wg-portal.util.merge" (list $ "wg-portal.destTemplate" "wg-portal.sourceTemplate") }}
|
||||
*/}}
|
||||
{{- define "wg-portal.util.merge" -}}
|
||||
{{- $top := first . -}}
|
||||
{{- $overrides := index . 1 -}}
|
||||
{{- $base := fromYaml (include (index . 2) $top) | default (dict) -}}
|
||||
{{- if kindIs "string" $overrides -}}
|
||||
{{- $overrides = fromYaml (include $overrides $top) | default (dict) -}}
|
||||
{{- end -}}
|
||||
{{- toYaml (merge $overrides $base) -}}
|
||||
{{- end -}}
|
119
deploy/helm/templates/_pod.tpl
Normal file
@ -0,0 +1,119 @@
|
||||
{{- define "wg-portal.podTemplate" -}}
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
|
||||
kubectl.kubernetes.io/default-container: {{ .Chart.Name }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
labels: {{- include "wg-portal.util.merge" (list $ .Values.podLabels "wg-portal.selectorLabels") | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.affinity }}
|
||||
affinity: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
containers:
|
||||
{{- with .Values.sidecarContainers }}
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag}}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- with .Values.command }}
|
||||
command: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.args }}
|
||||
args: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.env }}
|
||||
env: {{- tpl (toYaml .) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.envFrom }}
|
||||
envFrom: {{- tpl (toYaml .) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: {{ .Values.service.metrics.port}}
|
||||
protocol: TCP
|
||||
- name: web
|
||||
containerPort: {{ .Values.service.web.port }}
|
||||
protocol: TCP
|
||||
{{- range $index, $port := .Values.service.wireguard.ports }}
|
||||
- name: wg{{ $index }}
|
||||
containerPort: {{ $port }}
|
||||
protocol: UDP
|
||||
{{- end }}
|
||||
{{- with .Values.livenessProbe }}
|
||||
livenessProbe: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.readinessProbe }}
|
||||
readinessProbe: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.startupProbe }}
|
||||
startupProbe: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.securityContext }}
|
||||
securityContext: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.resources}}
|
||||
resources: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /app/config
|
||||
readOnly: true
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
|
||||
- name: certs
|
||||
mountPath: /app/certs
|
||||
{{- end }}
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- tpl (toYaml .) $ | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.dnsPolicy }}
|
||||
dnsPolicy: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.hostNetwork }}
|
||||
hostNetwork: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.initContainers }}
|
||||
initContainers: {{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.restartPolicy }}
|
||||
restartPolicy: {{ . }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "wg-portal.serviceAccountName" . }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: config
|
||||
secret:
|
||||
secretName: {{ include "wg-portal.fullname" . }}
|
||||
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
|
||||
- name: certs
|
||||
secret:
|
||||
secretName: {{ include "wg-portal.fullname" . }}-tls
|
||||
{{- end }}
|
||||
{{- if not .Values.persistence.enabled }}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- else if eq .Values.workloadType "Deployment" }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "wg-portal.fullname" . }}
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
66
deploy/helm/templates/_service.tpl
Normal file
@ -0,0 +1,66 @@
|
||||
{{/*
|
||||
Define the service template
|
||||
{{- include "wg-portal.service" (dict "context" $ "scope" .Values.service.<name> "ports" list "name" "<name>") -}}
|
||||
*/}}
|
||||
{{- define "wg-portal.service.tpl" -}}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
{{- with .scope.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels: {{- include "wg-portal.labels" .context | nindent 4 }}
|
||||
name: {{ include "wg-portal.fullname" .context }}{{ ternary "" (printf "-%s" .name) (empty .name) }}
|
||||
spec:
|
||||
{{- with .scope.clusterIP }}
|
||||
clusterIP: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.externalIPs }}
|
||||
externalIPs: {{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .scope.externalName }}
|
||||
externalName: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.externalTrafficPolicy }}
|
||||
externalTrafficPolicy: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.healthCheckNodePort }}
|
||||
healthCheckNodePort: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.loadBalancerIP }}
|
||||
loadBalancerIP: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.loadBalancerSourceRanges }}
|
||||
loadBalancerSourceRanges: {{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
ports: {{- toYaml .ports | nindent 4 }}
|
||||
{{- with .scope.publishNotReadyAddresses }}
|
||||
publishNotReadyAddresses: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.sessionAffinity }}
|
||||
sessionAffinity: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .scope.sessionAffinityConfig }}
|
||||
sessionAffinityConfig: {{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .scope.topologyKeys }}
|
||||
topologyKeys: {{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .scope.type }}
|
||||
type: {{ . }}
|
||||
{{- end }}
|
||||
selector: {{- include "wg-portal.selectorLabels" .context | nindent 4 }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Define the service port template for the web port
|
||||
*/}}
|
||||
{{- define "wg-portal.service.webPort" -}}
|
||||
name: web
|
||||
port: {{ .Values.service.web.port }}
|
||||
protocol: TCP
|
||||
targetPort: web
|
||||
{{- if semverCompare ">=1.20-0" .Capabilities.KubeVersion.Version }}
|
||||
appProtocol: {{ ternary "https" .Values.service.web.appProtocol .Values.certificate.enabled }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
54
deploy/helm/templates/certificate.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
{{/* https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources */}}
|
||||
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) -}}
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
secretName: {{ include "wg-portal.fullname" . }}-tls
|
||||
{{- with .Values.certificate.secretTemplate }}
|
||||
secretTemplate: {{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.privateKey }}
|
||||
privateKey: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.keystores }}
|
||||
keystores: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.duration }}
|
||||
duration: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.renewBefore }}
|
||||
renewBefore: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.usages }}
|
||||
usages: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.subject }}
|
||||
subject: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.commonName }}
|
||||
commonName: {{ . }}
|
||||
{{- end }}
|
||||
dnsNames:
|
||||
- {{ include "wg-portal.hostname" . }}
|
||||
{{- with .Values.certificate.uris }}
|
||||
uris: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.emailAddresses }}
|
||||
emailAddresses: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.ipAddresses }}
|
||||
ipAddresses: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.certificate.otherNames }}
|
||||
otherNames: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
issuerRef:
|
||||
{{- with .Values.certificate.issuer.group }}
|
||||
group: {{ . }}
|
||||
{{- end }}
|
||||
kind: {{ .Values.certificate.issuer.kind }}
|
||||
name: {{ .Values.certificate.issuer.name }}
|
||||
{{- end -}}
|
14
deploy/helm/templates/cm-dashboards.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
{{- with .Values.monitoring.dashboard -}}
|
||||
{{- if .enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
{{- with .annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
|
||||
name: {{ printf "grafana-dashboards-%s" (include "wg-portal.fullname" $) }}
|
||||
namespace: {{ default $.Release.Namespace .namespace }}
|
||||
data: {{ ($.Files.Glob "files/dashboard.json").AsConfig | nindent 2 }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
17
deploy/helm/templates/deployment.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
{{- if eq .Values.workloadType "Deployment" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.revisionHistoryLimit }}
|
||||
revisionHistoryLimit: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.strategy }}
|
||||
strategy: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
|
||||
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
|
||||
{{- end -}}
|
4
deploy/helm/templates/extras.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
{{- range .Values.extraDeploy -}}
|
||||
{{- tpl (toYaml .) $ }}
|
||||
---
|
||||
{{- end -}}
|
30
deploy/helm/templates/ingress.yaml
Normal file
@ -0,0 +1,30 @@
|
||||
{{- $hostname := include "wg-portal.hostname" . -}}
|
||||
{{- if and .Values.ingress.enabled $hostname -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ $hostname }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ default "/" (urlParse (tpl .Values.config.web.external_url .)).path }}
|
||||
pathType: {{ default "ImplementationSpecific" .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
port:
|
||||
name: web
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
- hosts:
|
||||
- {{ $hostname | quote }}
|
||||
secretName: {{ include "wg-portal.fullname" . }}-tls
|
||||
{{- end }}
|
||||
{{- end }}
|
44
deploy/helm/templates/monitoring.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
{{- with .Values.monitoring -}}
|
||||
{{- if and .enabled ($.Capabilities.APIVersions.Has .apiVersion) -}}
|
||||
{{- $endpointsKey := (eq .kind "PodMonitor") | ternary "podMetricsEndpoints" "endpoints" -}}
|
||||
apiVersion: {{ .apiVersion }}
|
||||
kind: {{ .kind }}
|
||||
metadata:
|
||||
{{- with .annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels: {{- include "wg-portal.util.merge" (list $ .labels "wg-portal.labels") | nindent 4 }}
|
||||
name: {{ include "wg-portal.fullname" $ }}
|
||||
spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- {{ $.Release.Namespace }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "wg-portal.selectorLabels" $ | nindent 6 }}
|
||||
{{ $endpointsKey }}:
|
||||
- port: metrics
|
||||
path: /metrics
|
||||
interval: {{ coalesce .interval ($.Values.config.statistics).data_collection_interval "1m" }}
|
||||
{{- with .metricRelabelings }}
|
||||
metricRelabelings: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
relabelings:
|
||||
- action: replace
|
||||
sourceLabels:
|
||||
- __meta_kubernetes_pod_label_app_kubernetes_io_name
|
||||
targetLabel: instance
|
||||
{{- with .relabelings }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .scrapeTimeout }}
|
||||
scrapeTimeout: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .jobLabel }}
|
||||
jobLabel: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .podTargetLabels }}
|
||||
podTargetLabels: {{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
11
deploy/helm/templates/pvc.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
{{- if and .Values.persistence.enabled (eq .Values.workloadType "Deployment") -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
{{- with .Values.persistence.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4}}
|
||||
{{- end }}
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
spec: {{- include "wg-portal.pvc" . | nindent 2 }}
|
||||
{{- end -}}
|
42
deploy/helm/templates/secret.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
{{- $advanced := dict "start_listen_port" (.Values.service.wireguard.ports | sortAlpha | first | int) -}}
|
||||
{{- $statistics := dict "listening_address" (printf ":%v" .Values.service.metrics.port) -}}
|
||||
{{- $web:= dict "listening_address" (printf ":%v" .Values.service.web.port) -}}
|
||||
{{- if and .Values.certificate.enabled (include "wg-portal.hostname" .) }}
|
||||
{{- $_ := set $web "cert_file" "/app/certs/tls.crt" }}
|
||||
{{- $_ := set $web "key_file" "/app/certs/tls.key" }}
|
||||
{{- end }}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
stringData:
|
||||
config.yml: |
|
||||
{{- with mustMerge $advanced .Values.config.advanced }}
|
||||
advanced: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.config.auth }}
|
||||
auth: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with mustMerge .Values.config.core (include "wg-portal.admin" . | fromYaml) }}
|
||||
core: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.config.database }}
|
||||
database: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with .Values.config.mail }}
|
||||
mail: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with mustMerge $statistics .Values.config.statistics }}
|
||||
statistics: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
||||
|
||||
{{- with mustMerge $web .Values.config.web }}
|
||||
web: {{- tpl (toYaml .) $ | nindent 6 }}
|
||||
{{- end }}
|
20
deploy/helm/templates/service.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
{{- $portsWeb := list (include "wg-portal.service.webPort" . | fromYaml) -}}
|
||||
{{- $ports := list -}}
|
||||
{{- range $idx, $port := .Values.service.wireguard.ports -}}
|
||||
{{- $name := printf "wg%d" $idx -}}
|
||||
{{- $ports = append $ports (dict "name" $name "port" $port "protocol" "UDP" "targetPort" $name) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if .Values.service.mixed.enabled -}}
|
||||
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.mixed "ports" (concat $portsWeb $ports)) }}
|
||||
{{- else }}
|
||||
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.web "ports" $portsWeb) }}
|
||||
---
|
||||
{{ include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.wireguard "ports" $ports "name" "wireguard") }}
|
||||
{{- end -}}
|
||||
|
||||
{{- if and .Values.monitoring.enabled (eq .Values.monitoring.kind "ServiceMonitor") }}
|
||||
---
|
||||
{{- $portsMetrics := list (dict "name" "metrics" "port" .Values.service.metrics.port "protocol" "TCP" "targetPort" "metrics") -}}
|
||||
{{- include "wg-portal.service.tpl" (dict "context" . "scope" .Values.service.metrics "ports" $portsWeb "name" "metrics") }}
|
||||
{{- end -}}
|
10
deploy/helm/templates/serviceaccount.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "wg-portal.serviceAccountName" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
24
deploy/helm/templates/statefulset.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
{{- if eq .Values.workloadType "StatefulSet" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "wg-portal.fullname" . }}
|
||||
labels: {{- include "wg-portal.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- with .Values.revisionHistoryLimit }}
|
||||
revisionHistoryLimit: {{ . }}
|
||||
{{- end }}
|
||||
{{- with .Values.strategy }}
|
||||
updateStrategy: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
serviceName: {{ template "wg-portal.fullname" . }}-web
|
||||
selector:
|
||||
matchLabels: {{- include "wg-portal.selectorLabels" . | nindent 6 }}
|
||||
template: {{- include "wg-portal.podTemplate" . | nindent 4 }}
|
||||
{{- if .Values.persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec: {{- include "wg-portal.pvc" . | nindent 8 }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
248
deploy/helm/values.yaml
Normal file
@ -0,0 +1,248 @@
|
||||
# Default values for wg-portal.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# -- Partially override resource names (adds suffix)
|
||||
nameOverride: ""
|
||||
# -- Fully override resource names
|
||||
fullnameOverride: ""
|
||||
# -- Array of extra objects to deploy with the release
|
||||
extraDeploy: []
|
||||
|
||||
config:
|
||||
# -- (tpl/object) [Advanced configuration](https://wgportal.org/latest/documentation/configuration/overview/#advanced) options.
|
||||
advanced: {}
|
||||
# -- (tpl/object) [Auth configuration](https://wgportal.org/latest/documentation/configuration/overview/#auth) options.
|
||||
auth: {}
|
||||
# -- (tpl/object) [Core configuration](https://wgportal.org/latest/documentation/configuration/overview/#core) options.<br>
|
||||
# If external admins in `auth` are defined and
|
||||
# there are no `admin_user` and `admin_password` defined here,
|
||||
# the default admin account will be disabled.
|
||||
core: {}
|
||||
# -- (tpl/object) [Database configuration](https://wgportal.org/latest/documentation/configuration/overview/#database) options
|
||||
database: {}
|
||||
# -- (tpl/object) [Mail configuration](https://wgportal.org/latest/documentation/configuration/overview/#mail) options
|
||||
mail: {}
|
||||
# -- (tpl/object) [Statistics configuration](https://wgportal.org/latest/documentation/configuration/overview/#statistics) options
|
||||
statistics: {}
|
||||
# -- (tpl/object) [Web configuration](https://wgportal.org/latest/documentation/configuration/overview/#web) options.<br>
|
||||
# `listening_address` will be set automatically from `service.web.port`.
|
||||
# `external_url` is required to enable ingress and certificate resources.
|
||||
web: {}
|
||||
|
||||
# -- The number of old ReplicaSets to retain to allow rollback.
|
||||
# @default -- `10`
|
||||
revisionHistoryLimit: ""
|
||||
# -- Workload type - `Deployment` or `StatefulSet`
|
||||
workloadType: Deployment
|
||||
# -- Update strategy for the workload
|
||||
# Valid values are:
|
||||
# `RollingUpdate` or `Recreate` for Deployment,
|
||||
# `RollingUpdate` or `OnDelete` for StatefulSet
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
|
||||
image:
|
||||
# -- Image repository
|
||||
repository: ghcr.io/h44z/wg-portal
|
||||
# -- Image pull policy
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Overrides the image tag whose default is the chart appVersion
|
||||
tag: ""
|
||||
|
||||
# -- Image pull secrets
|
||||
imagePullSecrets: []
|
||||
# -- (tpl/object) Extra annotations to add to the pod
|
||||
podAnnotations: {}
|
||||
# -- Extra labels to add to the pod
|
||||
podLabels: {}
|
||||
# -- Pod Security Context
|
||||
podSecurityContext: {}
|
||||
# Container Security Context
|
||||
securityContext:
|
||||
capabilities:
|
||||
# -- Add capabilities to the container
|
||||
add:
|
||||
- NET_ADMIN
|
||||
|
||||
# -- (tpl/list) Pod init containers
|
||||
initContainers: []
|
||||
# -- (tpl/list) Pod sidecar containers
|
||||
sidecarContainers: []
|
||||
# -- Set DNS policy for the pod.
|
||||
# Valid values are `ClusterFirstWithHostNet`, `ClusterFirst`, `Default` or `None`.
|
||||
# @default -- `"ClusterFirst"`
|
||||
dnsPolicy: ""
|
||||
# -- Restart policy for all containers within the pod.
|
||||
# Valid values are `Always`, `OnFailure` or `Never`.
|
||||
# @default -- `"Always"`
|
||||
restartPolicy: ""
|
||||
# -- Use the host's network namespace.
|
||||
# @default -- `false`.
|
||||
hostNetwork: ""
|
||||
# -- Resources requests and limits
|
||||
resources: {}
|
||||
# -- Overwrite pod command
|
||||
command: []
|
||||
# -- Additional pod arguments
|
||||
args: []
|
||||
# -- (tpl/list) Additional environment variables
|
||||
env: []
|
||||
# -- (tpl/list) Additional environment variables from a secret or configMap
|
||||
envFrom: []
|
||||
# -- Liveness probe configuration
|
||||
livenessProbe: {}
|
||||
# -- Readiness probe configuration
|
||||
readinessProbe: {}
|
||||
# -- Startup probe configuration
|
||||
startupProbe: {}
|
||||
# -- (tpl/list) Additional volumes
|
||||
volumes: []
|
||||
# -- (tpl/list) Additional volumeMounts
|
||||
volumeMounts: []
|
||||
# -- Node Selector configuration
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
# -- Tolerations configuration
|
||||
tolerations: []
|
||||
# -- Affinity configuration
|
||||
affinity: {}
|
||||
|
||||
service:
|
||||
mixed:
|
||||
# -- Whether to create a single service for the web and wireguard interfaces
|
||||
enabled: false
|
||||
# -- Service type
|
||||
type: LoadBalancer
|
||||
web:
|
||||
# -- Annotations for the web service
|
||||
annotations: {}
|
||||
# -- Web service type
|
||||
type: ClusterIP
|
||||
# -- Web service port
|
||||
# Used for the web interface listener
|
||||
port: 8888
|
||||
# -- Web service appProtocol. Will be auto set to `https` if certificate is enabled.
|
||||
appProtocol: http
|
||||
wireguard:
|
||||
# -- Annotations for the WireGuard service
|
||||
annotations: {}
|
||||
# -- Wireguard service type
|
||||
type: LoadBalancer
|
||||
# -- Wireguard service ports.
|
||||
# Exposes the WireGuard ports for created interfaces.
|
||||
# Lowerest port is selected as start port for the first interface.
|
||||
# Increment next port by 1 for each additional interface.
|
||||
ports:
|
||||
- 51820
|
||||
metrics:
|
||||
port: 8787
|
||||
|
||||
ingress:
|
||||
# -- Specifies whether an ingress resource should be created
|
||||
enabled: false
|
||||
# -- Ingress class name
|
||||
className: ""
|
||||
# -- Ingress annotations
|
||||
annotations: {}
|
||||
# -- Ingress TLS configuration.
|
||||
# Enable certificate resource or add ingress annotation to create required secret
|
||||
tls: false
|
||||
|
||||
certificate:
|
||||
# -- Specifies whether a certificate resource should be created.
|
||||
# If enabled, certificate will be used for the web.
|
||||
enabled: false
|
||||
issuer:
|
||||
# -- Certificate issuer name
|
||||
name: ""
|
||||
# -- Certificate issuer kind (ClusterIssuer or Issuer)
|
||||
kind: ""
|
||||
# -- Certificate issuer group
|
||||
group: cert-manager.io
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
duration: ""
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
renewBefore: ""
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
commonName: ""
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
emailAddresses: []
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
ipAddresses: []
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
keystores: {}
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
privateKey: {}
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
secretTemplate: {}
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
subject: {}
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
uris: []
|
||||
# -- Optional. [Documentation](https://cert-manager.io/docs/usage/certificate/#creating-certificate-resources)
|
||||
usages: []
|
||||
|
||||
persistence:
|
||||
# -- Specifies whether an persistent volume should be created
|
||||
enabled: false
|
||||
# -- Persistent Volume Claim annotations
|
||||
annotations: {}
|
||||
# -- Persistent Volume storage class.
|
||||
# If undefined (the default) cluster's default provisioner will be used.
|
||||
storageClass: ""
|
||||
# -- Persistent Volume Access Mode
|
||||
accessMode: ReadWriteOnce
|
||||
# -- Persistent Volume size
|
||||
size: 1Gi
|
||||
# -- Persistent Volume Name (optional)
|
||||
volumeName: ""
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Service account annotations
|
||||
annotations: {}
|
||||
# -- Automatically mount a ServiceAccount's API credentials
|
||||
automount: false
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
monitoring:
|
||||
# -- Enable Prometheus monitoring.
|
||||
enabled: false
|
||||
# -- API version of the Prometheus resource.
|
||||
# Use `azmonitoring.coreos.com/v1` for Azure Managed Prometheus.
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
# -- Kind of the Prometheus resource.
|
||||
# Could be `PodMonitor` or `ServiceMonitor`.
|
||||
kind: PodMonitor
|
||||
# -- Resource labels.
|
||||
labels: {}
|
||||
# -- Resource annotations.
|
||||
annotations: {}
|
||||
# -- Interval at which metrics should be scraped. If not specified `config.statistics.data_collection_interval` interval is used.
|
||||
# @default -- `1m`
|
||||
interval: ""
|
||||
# -- Relabelings to samples before ingestion.
|
||||
metricRelabelings: []
|
||||
# -- Relabelings to samples before scraping.
|
||||
relabelings: []
|
||||
# -- Timeout after which the scrape is ended If not specified, the Prometheus global scrape interval is used.
|
||||
scrapeTimeout: ""
|
||||
# -- The label to use to retrieve the job name from.
|
||||
jobLabel: ""
|
||||
# -- Transfers labels on the Kubernetes Pod onto the target.
|
||||
podTargetLabels: {}
|
||||
|
||||
dashboard:
|
||||
# -- Enable Grafana dashboard.
|
||||
enabled: false
|
||||
# -- Annotations for the dashboard ConfigMap.
|
||||
annotations: {}
|
||||
# -- Additional labels for the dashboard ConfigMap.
|
||||
labels: {}
|
||||
# -- Dashboard ConfigMap namespace
|
||||
# Overrides the namespace for the dashboard ConfigMap.
|
||||
namespace: ""
|
@ -1,8 +1,7 @@
|
||||
---
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v1
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
@ -11,9 +10,13 @@ services:
|
||||
max-file: "3"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
# Use host network mode for WireGuard and the UI. Ensure that access to the UI is properly secured.
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
# left side is the host path, right side is the container path
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
- EXTERNAL_URL=http://localhost:8123
|
||||
- ./config:/app/config
|
||||
# restart: no
|
||||
# command: ["-migrateFrom=/app/data/wg_portal.db"]
|
||||
|
||||
|
1
docs/CNAME
Normal file
@ -0,0 +1 @@
|
||||
wgportal.org
|
BIN
docs/assets/images/dashboard.png
Executable file
After Width: | Height: | Size: 269 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
2
docs/assets/images/logo.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>WireGuard icon</title><path d="M23.98 11.645S24.533 0 11.735 0C.418 0 .064 11.17.064 11.17S-1.6 24 11.997 24C25.04 24 23.98 11.645 23.98 11.645zM8.155 7.576c2.4-1.47 5.469-.571 6.618 1.638.218.419.246 1.063.108 1.503-.477 1.516-1.601 2.366-3.145 2.728.455-.39.817-.832.933-1.442a2.112 2.112 0 0 0-.364-1.677 2.14 2.14 0 0 0-2.465-.75c-.95.36-1.47 1.228-1.377 2.294.087.99.839 1.632 2.245 1.876-.21.111-.372.193-.53.281a5.113 5.113 0 0 0-1.644 1.43c-.143.192-.24.208-.458.075-2.827-1.729-3.009-6.067.078-7.956zM6.04 18.258c-.455.116-.895.286-1.359.438.227-1.532 2.021-2.943 3.539-2.782a3.91 3.91 0 0 0-.74 2.072c-.504.093-.98.155-1.44.272zM15.703 3.3c.448.017.898.01 1.347.02a2.324 2.324 0 0 1 .334.047 3.249 3.249 0 0 1-.34.434c-.16.15-.341.296-.573.069-.055-.055-.187-.042-.283-.044-.447-.005-.894-.02-1.34-.003a8.323 8.323 0 0 0-1.154.118c-.072.013-.178.25-.146.338.078.207.191.435.359.567.619.49 1.277.928 1.9 1.413.604.472 1.167.99 1.51 1.7.446.928.46 1.9.267 2.877-.322 1.63-1.147 2.98-2.483 3.962-.538.395-1.205.62-1.821.903-.543.25-1.1.465-1.644.712-.98.446-1.53 1.51-1.369 2.615.149 1.015 1.04 1.862 2.059 2.037 1.223.21 2.486-.586 2.785-1.83.336-1.397-.423-2.646-1.845-3.024l-.256-.066c.38-.17.708-.291 1.012-.458q.793-.437 1.558-.925c.15-.096.231-.096.36.014.977.846 1.56 1.898 1.724 3.187.27 2.135-.74 4.096-2.646 5.101-2.948 1.555-6.557-.215-7.208-3.484-.558-2.8 1.418-5.34 3.797-5.83 1.023-.211 1.958-.637 2.685-1.425.47-.508.697-.944.775-1.141a3.165 3.165 0 0 0 .217-1.158 2.71 2.71 0 0 0-.237-.992c-.248-.566-1.2-1.466-1.435-1.656l-2.24-1.754c-.079-.065-.168-.06-.36-.047-.23.016-.815.048-1.067-.018.204-.155.76-.38 1-.56-.726-.49-1.554-.314-2.315-.46.176-.328 1.046-.831 1.541-.888a7.323 7.323 0 0 0-.135-.822c-.03-.111-.154-.22-.263-.283-.262-.154-.541-.281-.843-.434a1.755 1.755 0 0 1 .906-.28 3.385 3.385 0 0 1 .908.088c.54.123.97.042 1.399-.324-.338-.136-.676-.26-1.003-.407a9.843 9.843 0 0 1-.942-.493c.85.118 1.671.437 2.54.32l.022-.118-2.018-.47c1.203-.11 2.323-.128 3.384.388.299.146.61.266.897.432.14.08.233.24.348.365.09.098.164.23.276.29.424.225.89.234 1.366.223l.01-.16c.479.15 1.017.702 1.017 1.105-.776 0-1.55-.003-2.325.004-.083 0-.165.061-.247.094.078.046.155.128.235.131z M14.703 2.153a.118.118 0 0 0-.016.19.179.179 0 0 0 .246.065c.075-.038.148-.078.238-.125-.072-.062-.13-.114-.19-.163-.106-.087-.193-.032-.278.033z"/></svg>
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/assets/images/screenshot.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
docs/assets/images/wg-tool.png
Normal file
After Width: | Height: | Size: 100 KiB |
190
docs/documentation/configuration/examples.md
Normal file
@ -0,0 +1,190 @@
|
||||
Below are some sample YAML configurations demonstrating how to override some default values.
|
||||
|
||||
## Basic
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@example.com
|
||||
admin_password: password
|
||||
admin_api_token: super-s3cr3t-api-token-or-a-UUID
|
||||
import_existing: false
|
||||
create_default_peer: true
|
||||
self_provisioning_allowed: true
|
||||
|
||||
web:
|
||||
site_title: My WireGuard Server
|
||||
site_company_name: My Company
|
||||
listening_address: :8080
|
||||
external_url: https://my.external-domain.com
|
||||
csrf_secret: super-s3cr3t-csrf
|
||||
session_secret: super-s3cr3t-session
|
||||
request_logging: true
|
||||
|
||||
advanced:
|
||||
log_level: trace
|
||||
log_pretty: true
|
||||
log_json: false
|
||||
config_storage_path: /etc/wireguard
|
||||
expiry_check_interval: 5m
|
||||
|
||||
database:
|
||||
debug: true
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
|
||||
```
|
||||
|
||||
## LDAP Authentication and Synchronization
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
ldap:
|
||||
# a sample LDAP provider with user sync enabled
|
||||
- id: ldap
|
||||
provider_name: Active Directory
|
||||
url: ldap://srv-ad1.company.local:389
|
||||
bind_user: ldap_wireguard@company.local
|
||||
bind_pass: super-s3cr3t-ldap
|
||||
base_dn: DC=COMPANY,DC=LOCAL
|
||||
login_filter: (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
sync_interval: 15m
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
disable_missing: true
|
||||
field_map:
|
||||
user_identifier: sAMAccountName
|
||||
email: mail
|
||||
firstname: givenName
|
||||
lastname: sn
|
||||
phone: telephoneNumber
|
||||
department: department
|
||||
memberof: memberOf
|
||||
admin_group: CN=WireGuardAdmins,OU=Some-OU,DC=COMPANY,DC=LOCAL
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## OpenID Connect (OIDC) Authentication
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oidc:
|
||||
# a sample Entra ID provider with environment variable substitution
|
||||
- id: azure
|
||||
provider_name: azure
|
||||
display_name: Login with</br>Entra ID
|
||||
registration_enabled: true
|
||||
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
||||
client_id: "${AZURE_CLIENT_ID}"
|
||||
client_secret: "${AZURE_CLIENT_SECRET}"
|
||||
extra_scopes:
|
||||
- profile
|
||||
- email
|
||||
|
||||
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
|
||||
- id: oidc-with-admin-attribute
|
||||
provider_name: google
|
||||
display_name: Login with</br>Google
|
||||
base_url: https://accounts.google.com
|
||||
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
is_admin: wg_admin
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
|
||||
# a sample provider where users in the group `the-admin-group` are considered as admins
|
||||
- id: oidc-with-admin-group
|
||||
provider_name: google2
|
||||
display_name: Login with</br>Google2
|
||||
base_url: https://accounts.google.com
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: given_name
|
||||
lastname: family_name
|
||||
phone: phone_number
|
||||
department: department
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_group_regex: ^the-admin-group$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
||||
|
||||
## Plain OAuth2 Authentication
|
||||
|
||||
```yaml
|
||||
# ... (basic configuration)
|
||||
|
||||
auth:
|
||||
oauth:
|
||||
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||
# are considered as admins
|
||||
- id: google_plain_oauth-with-admin-attribute
|
||||
provider_name: google3
|
||||
display_name: Login with</br>Google3
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
field_map:
|
||||
user_identifier: sub
|
||||
email: email
|
||||
firstname: name
|
||||
is_admin: this-attribute-must-be-true
|
||||
admin_mapping:
|
||||
admin_value_regex: ^(True|true)$
|
||||
registration_enabled: true
|
||||
|
||||
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||
# users in the group `admin-group-name` are considered as admins
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
auth_url: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token_url: https://oauth2.googleapis.com/token
|
||||
user_info_url: https://openidconnect.googleapis.com/v1/userinfo
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
- i-want-some-groups
|
||||
field_map:
|
||||
email: email
|
||||
firstname: name
|
||||
user_identifier: sub
|
||||
is_admin: this-attribute-must-be-true
|
||||
user_groups: groups
|
||||
admin_mapping:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
```
|
655
docs/documentation/configuration/overview.md
Normal file
@ -0,0 +1,655 @@
|
||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||
|
||||
You can supply these configurations in a **YAML** file when starting the Portal.
|
||||
The path of the configuration file defaults to `config/config.yaml` (or `config/config.yml`) in the working directory of the executable.
|
||||
It is possible to override the configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
||||
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
|
||||
Also, environment variable substitution in the config file is supported. Refer to the [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
|
||||
|
||||
Configuration examples are available on the [Examples](./examples.md) page.
|
||||
|
||||
<details>
|
||||
<summary>Default configuration</summary>
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: admin@wgportal.local
|
||||
admin_password: wgportal
|
||||
admin_api_token: ""
|
||||
editable_keys: true
|
||||
create_default_peer: false
|
||||
create_default_peer_on_creation: false
|
||||
re_enable_peer_after_user_enable: true
|
||||
delete_peer_after_user_deleted: false
|
||||
self_provisioning_allowed: false
|
||||
import_existing: true
|
||||
restore_state: true
|
||||
|
||||
advanced:
|
||||
log_level: info
|
||||
log_pretty: false
|
||||
log_json: false
|
||||
start_listen_port: 51820
|
||||
start_cidr_v4: 10.11.12.0/24
|
||||
start_cidr_v6: fdfd:d3ad:c0de:1234::0/64
|
||||
use_ip_v6: true
|
||||
config_storage_path: ""
|
||||
expiry_check_interval: 15m
|
||||
rule_prio_offset: 20000
|
||||
route_table_offset: 20000
|
||||
api_admin_only: true
|
||||
|
||||
database:
|
||||
debug: false
|
||||
slow_query_threshold: "0"
|
||||
type: sqlite
|
||||
dsn: data/sqlite.db
|
||||
encryption_passphrase: ""
|
||||
|
||||
statistics:
|
||||
use_ping_checks: true
|
||||
ping_check_workers: 10
|
||||
ping_unprivileged: false
|
||||
ping_check_interval: 1m
|
||||
data_collection_interval: 1m
|
||||
collect_interface_data: true
|
||||
collect_peer_data: true
|
||||
collect_audit_data: true
|
||||
listening_address: :8787
|
||||
|
||||
mail:
|
||||
host: 127.0.0.1
|
||||
port: 25
|
||||
encryption: none
|
||||
cert_validation: true
|
||||
username: ""
|
||||
password: ""
|
||||
auth_type: plain
|
||||
from: Wireguard Portal <noreply@wireguard.local>
|
||||
link_only: false
|
||||
|
||||
auth:
|
||||
oidc: []
|
||||
oauth: []
|
||||
ldap: []
|
||||
|
||||
web:
|
||||
listening_address: :8888
|
||||
external_url: http://localhost:8888
|
||||
site_company_name: WireGuard Portal
|
||||
site_title: WireGuard Portal
|
||||
session_identifier: wgPortalSession
|
||||
session_secret: very_secret
|
||||
csrf_secret: extremely_secret
|
||||
request_logging: false
|
||||
expose_host_info: false
|
||||
cert_file: ""
|
||||
key_File: ""
|
||||
|
||||
webhook:
|
||||
url: ""
|
||||
authentication: ""
|
||||
timeout: 10s
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Below you will find sections like
|
||||
[`core`](#core),
|
||||
[`advanced`](#advanced),
|
||||
[`database`](#database),
|
||||
[`statistics`](#statistics),
|
||||
[`mail`](#mail),
|
||||
[`auth`](#auth),
|
||||
[`web`](#web) and
|
||||
[`webhook`](#webhook).
|
||||
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
||||
|
||||
---
|
||||
|
||||
## Core
|
||||
|
||||
These are the primary configuration options that control fundamental WireGuard Portal behavior.
|
||||
More advanced options are found in the subsequent `Advanced` section.
|
||||
|
||||
### `admin_user`
|
||||
- **Default:** `admin@wgportal.local`
|
||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||
|
||||
### `admin_password`
|
||||
- **Default:** `wgportal`
|
||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
||||
|
||||
### `admin_api_token`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An API token for the admin user. If a token is provided, the REST API can be accessed using this token. If empty, the API is initially disabled for the admin user.
|
||||
|
||||
### `editable_keys`
|
||||
- **Default:** `true`
|
||||
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||
|
||||
### `create_default_peer`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `create_default_peer_on_creation`
|
||||
- **Default:** `false`
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `re_enable_peer_after_user_enable`
|
||||
- **Default:** `true`
|
||||
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
|
||||
|
||||
### `delete_peer_after_user_deleted`
|
||||
- **Default:** `false`
|
||||
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
|
||||
|
||||
### `self_provisioning_allowed`
|
||||
- **Default:** `false`
|
||||
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
|
||||
|
||||
### `import_existing`
|
||||
- **Default:** `true`
|
||||
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
|
||||
|
||||
### `restore_state`
|
||||
- **Default:** `true`
|
||||
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
|
||||
|
||||
---
|
||||
|
||||
## Advanced
|
||||
|
||||
Additional or more specialized configuration options for logging and interface creation details.
|
||||
|
||||
### `log_level`
|
||||
- **Default:** `info`
|
||||
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
|
||||
|
||||
### `log_pretty`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
|
||||
|
||||
### `log_json`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, log messages are structured in JSON format.
|
||||
|
||||
### `start_listen_port`
|
||||
- **Default:** `51820`
|
||||
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v4`
|
||||
- **Default:** `10.11.12.0/24`
|
||||
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v6`
|
||||
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
|
||||
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `use_ip_v6`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable or disable IPv6 support.
|
||||
|
||||
### `config_storage_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
|
||||
|
||||
### `expiry_check_interval`
|
||||
- **Default:** `15m`
|
||||
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `rule_prio_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route rule priorities when configuring routing.
|
||||
|
||||
### `route_table_offset`
|
||||
- **Default:** `20000`
|
||||
- **Description:** Offset for IP route table IDs when configuring routing.
|
||||
|
||||
### `api_admin_only`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||
|
||||
---
|
||||
|
||||
## Database
|
||||
|
||||
Configuration for the underlying database used by WireGuard Portal.
|
||||
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||
|
||||
If sensitive values (like private keys) should be stored in an encrypted format, set the `encryption_passphrase` option.
|
||||
|
||||
### `debug`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, logs all database statements (verbose).
|
||||
|
||||
### `slow_query_threshold`
|
||||
- **Default:** "0"
|
||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration). The value must be a string.
|
||||
|
||||
### `type`
|
||||
- **Default:** `sqlite`
|
||||
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
|
||||
|
||||
### `dsn`
|
||||
- **Default:** `data/sqlite.db`
|
||||
- **Description:** The Data Source Name (DSN) for connecting to the database.
|
||||
For example:
|
||||
```text
|
||||
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
```
|
||||
|
||||
### `encryption_passphrase`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Passphrase for encrypting sensitive values such as private keys in the database. Encryption is only applied if this passphrase is set.
|
||||
**Important:** Once you enable encryption by setting this passphrase, you cannot disable it or change it afterward.
|
||||
New or updated records will be encrypted; existing data remains in plaintext until it’s next modified.
|
||||
|
||||
---
|
||||
|
||||
## Statistics
|
||||
|
||||
Controls how WireGuard Portal collects and reports usage statistics, including ping checks and Prometheus metrics.
|
||||
|
||||
### `use_ping_checks`
|
||||
- **Default:** `true`
|
||||
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
|
||||
|
||||
### `ping_check_workers`
|
||||
- **Default:** `10`
|
||||
- **Description:** Number of parallel worker processes for ping checks.
|
||||
|
||||
### `ping_unprivileged`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
|
||||
|
||||
### `ping_check_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `data_collection_interval`
|
||||
- **Default:** `1m`
|
||||
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `collect_interface_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
|
||||
|
||||
### `collect_peer_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
|
||||
|
||||
### `collect_audit_data`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8787`
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
|
||||
|
||||
---
|
||||
|
||||
## Mail
|
||||
|
||||
Options for configuring email notifications or sending peer configurations via email.
|
||||
|
||||
### `host`
|
||||
- **Default:** `127.0.0.1`
|
||||
- **Description:** Hostname or IP of the SMTP server.
|
||||
|
||||
### `port`
|
||||
- **Default:** `25`
|
||||
- **Description:** Port number for the SMTP server.
|
||||
|
||||
### `encryption`
|
||||
- **Default:** `none`
|
||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||
|
||||
### `cert_validation`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||
|
||||
### `username`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP username for authentication.
|
||||
|
||||
### `password`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Optional SMTP password for authentication.
|
||||
|
||||
### `auth_type`
|
||||
- **Default:** `plain`
|
||||
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
|
||||
|
||||
### `from`
|
||||
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
|
||||
- **Description:** The default "From" address when sending emails.
|
||||
|
||||
### `link_only`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
|
||||
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
||||
Each can have multiple providers configured. Below are the relevant keys.
|
||||
|
||||
---
|
||||
|
||||
### OIDC
|
||||
|
||||
The `oidc` array contains a list of OpenID Connect providers.
|
||||
Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page (e.g., "Login with Google").
|
||||
|
||||
#### `base_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OIDC provider’s base URL (e.g., `https://accounts.google.com`).
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID from the OIDC provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret from the OIDC provider.
|
||||
|
||||
#### `extra_scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
||||
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
|
||||
|
||||
---
|
||||
|
||||
### OAuth
|
||||
|
||||
The `oauth` array contains a list of plain OAuth2 providers.
|
||||
Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A user-friendly name shown on the login page.
|
||||
|
||||
#### `client_id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client ID for the provider.
|
||||
|
||||
#### `client_secret`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The OAuth client secret for the provider.
|
||||
|
||||
#### `auth_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the authentication endpoint.
|
||||
|
||||
#### `token_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the token endpoint.
|
||||
|
||||
#### `user_info_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** URL of the user information endpoint.
|
||||
|
||||
#### `scopes`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of OAuth scopes.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||
|
||||
| **Field** | **Typical Claim** | **Explanation** |
|
||||
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||
| `lastname` | `family_name` | The user’s last (family) name, typically provided by the IdP in the `family_name` claim. |
|
||||
| `phone` | `phone_number` | The user’s phone number. This may require additional scopes/permissions from the IdP to access. |
|
||||
| `department` | Custom claim (e.g., `department`) | If the IdP can provide organizational data, it may store it in a custom claim. Adjust accordingly (e.g., `department`, `org`, or another attribute). |
|
||||
| `is_admin` | Custom claim or derived role | If the IdP returns a role or admin flag, you can map that to `is_admin`. Often this is managed through custom claims or group membership. |
|
||||
| `user_groups` | `groups` or another custom claim | A list of group memberships for the user. Some IdPs provide `groups` out of the box; others require custom claims or directory lookups. |
|
||||
|
||||
#### `admin_mapping`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** WgPortal can grant a user admin rights by matching the value of the `is_admin` claim against a regular expression. Alternatively, a regular expression can be used to check if a user is member of a specific group listed in the `user_group` claim. The regular expressions are defined in `admin_value_regex` and `admin_group_regex`.
|
||||
- `admin_value_regex`: A regular expression to match the `is_admin` claim. By default, this expression matches the string "true" (`^true$`).
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new users are created automatically on successful login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs user info at the trace level upon login.
|
||||
|
||||
---
|
||||
|
||||
### LDAP
|
||||
|
||||
The `ldap` array contains a list of LDAP authentication providers.
|
||||
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||
|
||||
#### `provider_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||
|
||||
#### `url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||
|
||||
#### `start_tls`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
|
||||
|
||||
#### `cert_validation`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, validate the LDAP server’s TLS certificate.
|
||||
|
||||
#### `tls_certificate_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to a TLS certificate if needed for LDAP connections.
|
||||
|
||||
#### `tls_key_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Path to the corresponding TLS certificate key.
|
||||
|
||||
#### `base_dn`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The base DN for user searches (e.g., `DC=COMPANY,DC=LOCAL`).
|
||||
|
||||
#### `bind_user`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind user for LDAP (e.g., `company\\ldap_wireguard` or `ldap_wireguard@company.local`).
|
||||
|
||||
#### `bind_pass`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The bind password for LDAP authentication.
|
||||
|
||||
#### `field_map`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Maps LDAP attributes to WireGuard Portal fields.
|
||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
||||
|
||||
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
||||
|----------------------------|----------------------------|--------------------------------------------------------------|
|
||||
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
||||
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
||||
| firstname | givenName | Contains the user's first (given) name. |
|
||||
| lastname | sn | Contains the user's last (surname) name. |
|
||||
| phone | telephoneNumber / mobile | Holds the user's phone or mobile number. |
|
||||
| department | departmentNumber / ou | Specifies the department or organizational unit of the user. |
|
||||
| memberof | memberOf | Lists the groups and roles to which the user belongs. |
|
||||
|
||||
#### `login_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to restrict which users can log in. Use `{{login_identifier}}` to insert the username.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||
```
|
||||
|
||||
#### `admin_group`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A specific LDAP group whose members are considered administrators in WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
CN=WireGuardAdmins,OU=Some-OU,DC=YOURDOMAIN,DC=LOCAL
|
||||
```
|
||||
|
||||
#### `sync_interval`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** How frequently (in duration, e.g. `30m`) to synchronize users from LDAP. Empty or `0` disables sync. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
Only users that match the `sync_filter` are synchronized, if `disable_missing` is `true`, users not found in LDAP are disabled.
|
||||
|
||||
#### `sync_filter`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** An LDAP filter to select which users get synchronized into WireGuard Portal.
|
||||
For example:
|
||||
```text
|
||||
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
```
|
||||
|
||||
#### `disable_missing`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
|
||||
|
||||
#### `auto_re_enable`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, users that where disabled because they were missing (see `disable_missing`) will be re-enabled once they are found again.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
||||
|
||||
---
|
||||
|
||||
## Web
|
||||
|
||||
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
|
||||
It is important to specify a valid `external_url` for the web server, especially if you are using a reverse proxy.
|
||||
Without a valid `external_url`, the login process may fail due to CSRF protection.
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8888`
|
||||
- **Description:** The listening address and port for the web server (e.g., `:8888` to bind on all interfaces or `127.0.0.1:8888` to bind only on the loopback interface).
|
||||
Ensure that access to WireGuard Portal is protected against unauthorized access, especially if binding to all interfaces.
|
||||
|
||||
### `external_url`
|
||||
- **Default:** `http://localhost:8888`
|
||||
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
||||
|
||||
### `site_company_name`
|
||||
- **Default:** `WireGuard Portal`
|
||||
- **Description:** The company name that is shown at the bottom of the web frontend.
|
||||
|
||||
### `site_title`
|
||||
- **Default:** `WireGuard Portal`
|
||||
- **Description:** The title that is shown in the web frontend.
|
||||
|
||||
### `session_identifier`
|
||||
- **Default:** `wgPortalSession`
|
||||
- **Description:** The session identifier for the web frontend.
|
||||
|
||||
### `session_secret`
|
||||
- **Default:** `very_secret`
|
||||
- **Description:** The session secret for the web frontend.
|
||||
|
||||
### `csrf_secret`
|
||||
- **Default:** `extremely_secret`
|
||||
- **Description:** The CSRF secret.
|
||||
|
||||
### `request_logging`
|
||||
- **Default:** `false`
|
||||
- **Description:** Log all HTTP requests.
|
||||
|
||||
### `expose_host_info`
|
||||
- **Default:** `false`
|
||||
- **Description:** Expose the hostname and version of the WireGuard Portal server in an HTTP header. This is useful for debugging but may expose sensitive information.
|
||||
|
||||
### `cert_file`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** (Optional) Path to the TLS certificate file.
|
||||
|
||||
### `key_file`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** (Optional) Path to the TLS certificate key file.
|
||||
|
||||
---
|
||||
|
||||
## Webhook
|
||||
|
||||
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
|
||||
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
||||
```json
|
||||
{
|
||||
"event": "peer_created",
|
||||
"entity": "peer",
|
||||
"identifier": "the-peer-identifier",
|
||||
"payload": {
|
||||
// The payload of the event, e.g. peer data.
|
||||
// Check the API documentation for the exact structure.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
|
||||
|
||||
### `authentication`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** The Authorization header for the webhook endpoint. The value is send as-is in the header. For example: `Bearer <token>`.
|
||||
|
||||
### `timeout`
|
||||
- **Default:** `10s`
|
||||
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.
|
42
docs/documentation/getting-started/binaries.md
Normal file
@ -0,0 +1,42 @@
|
||||
Starting from v2, each [release](https://github.com/h44z/wg-portal/releases) includes compiled binaries for supported platforms.
|
||||
These binary versions can be manually downloaded and installed.
|
||||
|
||||
## Download
|
||||
|
||||
Make sure that you download the correct binary for your architecture. The available binaries are:
|
||||
|
||||
- `wg-portal_linux_amd64` - Linux x86_64
|
||||
- `wg-portal_linux_arm64` - Linux ARM 64-bit
|
||||
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
|
||||
|
||||
With `curl`:
|
||||
|
||||
```shell
|
||||
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
|
||||
With `wget`:
|
||||
|
||||
```shell
|
||||
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||
```
|
||||
|
||||
with `gh cli`:
|
||||
|
||||
```shell
|
||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /opt/wg-portal
|
||||
sudo install wg-portal /opt/wg-portal/
|
||||
```
|
||||
|
||||
## Unreleased versions (master branch builds)
|
||||
|
||||
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).
|
||||
|
159
docs/documentation/getting-started/docker.md
Normal file
@ -0,0 +1,159 @@
|
||||
## Image Usage
|
||||
|
||||
The WireGuard Portal Docker image is available on both [Docker Hub](https://hub.docker.com/r/wgportal/wg-portal) and [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
||||
It is built on the official Alpine Linux base image and comes pre-packaged with all necessary WireGuard dependencies.
|
||||
|
||||
This container allows you to establish WireGuard VPN connections without relying on a host system that supports WireGuard or using the `linuxserver/wireguard` Docker image.
|
||||
|
||||
The recommended method for deploying WireGuard Portal is via Docker Compose for ease of configuration and management.
|
||||
|
||||
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
|
||||
|
||||
```yaml
|
||||
--8<-- "docker-compose.yml::19"
|
||||
```
|
||||
|
||||
By default, the webserver for the UI is listening on port **8888** on all available interfaces.
|
||||
|
||||
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
|
||||
|
||||
## WireGuard Interface Handling
|
||||
|
||||
WireGuard Portal supports managing WireGuard interfaces through three distinct deployment methods, providing flexibility based on your system architecture and operational preferences:
|
||||
|
||||
- **Directly on the host system**:
|
||||
WireGuard Portal can control WireGuard interfaces natively on the host, without using containers.
|
||||
This setup is ideal for environments where direct access to system networking is preferred.
|
||||
To use this method, you need to set the network mode to `host` in your docker-compose.yml file.
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
...
|
||||
network_mode: "host"
|
||||
...
|
||||
```
|
||||
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
|
||||
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
|
||||
|
||||
- **Within the WireGuard Portal Docker container**:
|
||||
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
|
||||
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
...
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||
- "51820:51820/udp"
|
||||
# Web UI port
|
||||
- "8888:8888/tcp"
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
```
|
||||
|
||||
- **Via a separate Docker container**:
|
||||
WireGuard Portal can interface with and control WireGuard running in another Docker container, such as the [linuxserver/wireguard](https://docs.linuxserver.io/images/docker-wireguard/) image.
|
||||
This method is useful in setups that already use `linuxserver/wireguard` or where you want to isolate the VPN backend from the portal frontend.
|
||||
For this, you need to set the network mode to `service:wireguard` in your docker-compose.yml file, `wireguard` is the service name of your WireGuard container.
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
...
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "service:wireguard" # So we ensure to stay on the same network as the wireguard container.
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/etc:/etc/wireguard
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
|
||||
wireguard:
|
||||
image: lscr.io/linuxserver/wireguard:latest
|
||||
container_name: wireguard
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
- "51820:51820/udp" # WireGuard port, needs to match the port in wg-portal interface config
|
||||
- "8888:8888/tcp" # Noticed that the port of the web UI is exposed in the wireguard container.
|
||||
volumes:
|
||||
- ./wg/etc:/config/wg_confs # We share the configuration (wgx.conf) between wg-portal and wireguard
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
```
|
||||
As the `linuxserver/wireguard` image uses _wg-quick_ to manage the interfaces, you need to have at least the following configuration set for WireGuard Portal:
|
||||
```yaml
|
||||
core:
|
||||
# The WireGuard container uses wg-quick to manage the WireGuard interfaces - this conflicts with WireGuard Portal during startup.
|
||||
# To avoid this, we need to set the restore_state option to false so that wg-quick can create the interfaces.
|
||||
restore_state: false
|
||||
# Usually, there are no existing interfaces in the WireGuard container, so we can set this to false.
|
||||
import_existing: false
|
||||
advanced:
|
||||
# WireGuard Portal needs to export the WireGuard configuration as wg-quick config files so that the WireGuard container can use them.
|
||||
config_storage_path: /etc/wireguard/
|
||||
```
|
||||
|
||||
## Image Versioning
|
||||
|
||||
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
||||
There are three types of tags in the repository:
|
||||
|
||||
#### Semantic versioned tags
|
||||
|
||||
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
|
||||
|
||||
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
|
||||
|
||||
Once these tags show up in this repository, they will never change.
|
||||
|
||||
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. `wgportal/wg-portal:2.0.0`, instead of the latest or canary tags.
|
||||
|
||||
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v2` or `2.0`.
|
||||
|
||||
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
||||
|
||||
#### latest
|
||||
|
||||
This is the most recent build to master! It changes a lot and is very unstable.
|
||||
|
||||
We recommend that you don't use it except for development purposes.
|
||||
|
||||
#### Branch tags
|
||||
|
||||
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can configure WireGuard Portal using a YAML configuration file.
|
||||
The filepath of the YAML configuration file defaults to `/app/config/config.yaml`.
|
||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
||||
|
||||
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
|
||||
|
||||
You should mount those directories as a volume:
|
||||
|
||||
- `/app/data`
|
||||
- `/app/config`
|
||||
|
||||
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||
|
||||
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory inside the container to a location of your choice.
|
||||
Also enable the `config_storage_path` option in the configuration file:
|
||||
```yaml
|
||||
advanced:
|
||||
config_storage_path: /etc/wireguard
|
||||
```
|
1
docs/documentation/getting-started/helm.md
Normal file
@ -0,0 +1 @@
|
||||
--8<-- "./deploy/helm/README.md:16"
|
98
docs/documentation/getting-started/reverse-proxy.md
Normal file
@ -0,0 +1,98 @@
|
||||
## Reverse Proxy for HTTPS
|
||||
|
||||
For production deployments, always serve the WireGuard Portal over HTTPS. You have two options to secure your connection:
|
||||
|
||||
|
||||
### Reverse Proxy
|
||||
|
||||
Let a front‐end proxy handle HTTPS for you. This also frees you from managing certificates manually and is therefore the preferred option.
|
||||
You can use Nginx, Traefik, Caddy or any other proxy.
|
||||
|
||||
Below is an example using a Docker Compose stack with [Traefik](https://traefik.io/traefik/).
|
||||
It exposes the WireGuard Portal on `https://wg.domain.com` and redirects initial HTTP traffic to HTTPS.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
reverse-proxy:
|
||||
image: traefik:v3.3
|
||||
restart: unless-stopped
|
||||
command:
|
||||
#- '--log.level=DEBUG'
|
||||
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
|
||||
- '--providers.docker.exposedbydefault=false'
|
||||
- '--entrypoints.web.address=:80'
|
||||
- '--entrypoints.websecure.address=:443'
|
||||
- '--entrypoints.websecure.http3'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge=true'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge.entrypoint=web'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.email=your.email@domain.com'
|
||||
- '--certificatesresolvers.letsencryptresolver.acme.storage=/letsencrypt/acme.json'
|
||||
#- '--certificatesresolvers.letsencryptresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory' # just for testing
|
||||
ports:
|
||||
- 80:80 # for HTTP
|
||||
- 443:443/tcp # for HTTPS
|
||||
- 443:443/udp # for HTTP/3
|
||||
volumes:
|
||||
- acme-certs:/letsencrypt
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
# HTTP Catchall for redirecting HTTP -> HTTPS
|
||||
- 'traefik.http.routers.dashboard-catchall.rule=Host(`wg.domain.com`) && PathPrefix(`/`)'
|
||||
- 'traefik.http.routers.dashboard-catchall.entrypoints=web'
|
||||
- 'traefik.http.routers.dashboard-catchall.middlewares=redirect-to-https'
|
||||
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
|
||||
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
container_name: wg-portal
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
ports:
|
||||
# host port : container port
|
||||
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||
- "51820:51820/udp"
|
||||
# Web UI port (only available on localhost, Traefik will handle the HTTPS)
|
||||
- "127.0.0.1:8888:8888/tcp"
|
||||
sysctls:
|
||||
- net.ipv4.conf.all.src_valid_mark=1
|
||||
volumes:
|
||||
# host path : container path
|
||||
- ./wg/data:/app/data
|
||||
- ./wg/config:/app/config
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.wgportal.rule=Host(`wg.domain.com`)'
|
||||
- 'traefik.http.routers.wgportal.entrypoints=websecure'
|
||||
- 'traefik.http.routers.wgportal.tls.certresolver=letsencryptresolver'
|
||||
- 'traefik.http.routers.wgportal.service=wgportal'
|
||||
- 'traefik.http.services.wgportal.loadbalancer.server.port=8888'
|
||||
|
||||
volumes:
|
||||
acme-certs:
|
||||
```
|
||||
|
||||
The WireGuard Portal configuration must be updated accordingly so that the correct external URL is set for the web interface:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
external_url: https://wg.domain.com
|
||||
```
|
||||
|
||||
### Built-in TLS
|
||||
|
||||
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||
In your `config.yaml`, under the `web` section, point to your certificate and key files:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
cert_file: /path/to/your/fullchain.pem
|
||||
key_file: /path/to/your/privkey.pem
|
||||
```
|
||||
|
||||
The web server will then use these files to serve HTTPS traffic directly instead of HTTP.
|
26
docs/documentation/getting-started/sources.md
Normal file
@ -0,0 +1,26 @@
|
||||
To build the application from source files, use the Makefile provided in the repository.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [Make](https://www.gnu.org/software/make/)
|
||||
- [Go](https://go.dev/dl/): `>=1.24.0`
|
||||
- [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
|
||||
|
||||
## Build
|
||||
|
||||
```shell
|
||||
# Get source code
|
||||
git clone https://github.com/h44z/wg-portal -b ${WG_PORTAL_VERSION:-master} --depth 1
|
||||
cd wg-portal
|
||||
# Build the frontend
|
||||
make frontend
|
||||
# Build the backend
|
||||
make build
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Compiled binary will be available in `./dist` directory.
|
||||
|
||||
For installation instructions, check the [Binaries](./binaries.md) section.
|
32
docs/documentation/monitoring/prometheus.md
Normal file
@ -0,0 +1,32 @@
|
||||
By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
|
||||
|
||||
## Exposed Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------------------------------------------|-------|------------------------------------------------|
|
||||
| `wireguard_interface_received_bytes_total` | gauge | Bytes received through the interface. |
|
||||
| `wireguard_interface_sent_bytes_total` | gauge | Bytes sent through the interface. |
|
||||
| `wireguard_peer_last_handshake_seconds` | gauge | Seconds from the last handshake with the peer. |
|
||||
| `wireguard_peer_received_bytes_total` | gauge | Bytes received from the peer. |
|
||||
| `wireguard_peer_sent_bytes_total` | gauge | Bytes sent to the peer. |
|
||||
| `wireguard_peer_up` | gauge | Peer connection state (boolean: 1/0). |
|
||||
|
||||
## Prometheus Config
|
||||
|
||||
Add the following scrape job to your Prometheus config file:
|
||||
|
||||
```yaml
|
||||
# prometheus.yaml
|
||||
scrape_configs:
|
||||
- job_name: wg-portal
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8787 # Change localhost to IP Address or hostname with WG-Portal
|
||||
```
|
||||
|
||||
# Grafana Dashboard
|
||||
|
||||
You may import [`dashboard.json`](https://github.com/h44z/wg-portal/blob/master/deploy/helm/files/dashboard.json) into your Grafana instance.
|
||||
|
||||

|
1
docs/documentation/overview.md
Normal file
@ -0,0 +1 @@
|
||||
--8<-- "README.md:12:41"
|
1
docs/documentation/rest-api/api-doc.md
Normal file
@ -0,0 +1 @@
|
||||
<swagger-ui src="./swagger.yaml"/>
|
1487
docs/documentation/rest-api/swagger.yaml
Normal file
37
docs/documentation/upgrade/v1.md
Normal file
@ -0,0 +1,37 @@
|
||||
Major upgrades between different versions may require special procedures, which are described in the following sections.
|
||||
|
||||
## Upgrade from v1 to v2
|
||||
|
||||
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
|
||||
|
||||
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
|
||||
The configuration (config.yaml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
||||
|
||||
To upgrade from a previous SQLite database, start wg-portal like:
|
||||
|
||||
```shell
|
||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
||||
```
|
||||
|
||||
You can also specify the database type using the parameter **-migrateFromType**.
|
||||
Supported database types: `mysql`, `mssql`, `postgres` or `sqlite`.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
|
||||
```
|
||||
|
||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yaml** configuration file.
|
||||
Ensure that the new database does not contain any data!
|
||||
|
||||
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
# ... other settings
|
||||
restart: no
|
||||
command: ["-migrateFrom=/app/data/old_wg_portal.db"]
|
||||
```
|
4
docs/index.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
template: layouts/home.html
|
||||
title: WireGuard Portal
|
||||
---
|
49
docs/stylesheets/extra.css
Normal file
@ -0,0 +1,49 @@
|
||||
/* This file is used for extra styles that are not part of the theme */
|
||||
|
||||
span.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.em {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-bottom: 1px solid #e3e8ee;
|
||||
}
|
||||
|
||||
a.field {
|
||||
font-weight: 600;
|
||||
/* color: #3c4257; */
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
span.parent-field {
|
||||
font-weight: 600;
|
||||
color:#a3acb9;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
span.type {
|
||||
color: #8792a2;
|
||||
font-size: .7rem;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
span.version {
|
||||
color: #8792a2;
|
||||
font-size: .7rem;
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.faint {
|
||||
color: #8792a2;
|
||||
}
|
||||
|
||||
.md-social__link svg {
|
||||
fill: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.md-tabs__link {
|
||||
font-size: 0.8rem;
|
||||
}
|
433
docs/theme-overrides/layouts/home.html
Normal file
@ -0,0 +1,433 @@
|
||||
|
||||
{% extends "main.html" %}
|
||||
|
||||
<!-- Render hero under tabs -->
|
||||
{% block tabs %}
|
||||
{{ super() }}
|
||||
|
||||
<!-- Additional styles for landing page -->
|
||||
<style>
|
||||
|
||||
/* Apply box shadow on smaller screens that don't display tabs */
|
||||
@media only screen and (max-width: 1220px) {
|
||||
.md-header {
|
||||
box-shadow: 0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);
|
||||
transition: color 250ms,background-color 250ms,box-shadow 250ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide main content for now */
|
||||
.md-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide table of contents */
|
||||
@media screen and (min-width: 60em) {
|
||||
.md-sidebar--secondary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide navigation */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar--primary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get started button */
|
||||
.md-typeset .md-button--primary {
|
||||
color: var(--md-primary-fg-color);
|
||||
background-color: var(--md-primary-bg-color);
|
||||
border-color: var(--md-primary-bg-color);
|
||||
}
|
||||
.md-typeset .md-button--primary:hover {
|
||||
color: var(--md-primary-bg-color);
|
||||
background-color: var(--md-primary-fg-color);
|
||||
border-color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
.tx-hero {
|
||||
max-width: 700px;
|
||||
display: flex;
|
||||
padding: .4rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.tx-hero h1 {
|
||||
font-weight: 700;
|
||||
font-size: 38px;
|
||||
line-height: 46px;
|
||||
color: rgb(38, 38, 38);
|
||||
}
|
||||
.tx-hero p {
|
||||
color: rgb(92, 92, 92);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.tx-hero__image {
|
||||
max-width: 1000px;
|
||||
min-width: 600px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tx-hero__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Secondary content styles */
|
||||
.secondary-section {
|
||||
background: rgb(245, 245, 245) none repeat scroll 0% 0%;
|
||||
border-top: 1px solid rgb(222, 222, 222);
|
||||
border-bottom: 1px solid rgb(222, 222, 222)
|
||||
}
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0px 40px;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 30px;
|
||||
letter-spacing: normal;
|
||||
padding: 88px 0px 116px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section.follow {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
|
||||
.secondary-section .g .section .component-wrapper {
|
||||
display: flex;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper h3 {
|
||||
color: rgb(38, 38, 38);
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 46px;
|
||||
letter-spacing: normal;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper h4 {
|
||||
color: rgb(38, 38, 38);
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper p {
|
||||
color: rgb(92, 92, 92);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 30px;
|
||||
letter-spacing: normal;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .image-wrapper {
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
margin-top: 48px;
|
||||
border: 1px solid rgb(222, 222, 222);
|
||||
box-shadow: rgba(202, 202, 202, 0.15) 0px 0px 0px 6px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .first-column {
|
||||
padding-right: 100px;
|
||||
flex: 0 1 auto;
|
||||
height: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper .first-column {
|
||||
padding-right: 0px;
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .second-column {
|
||||
flex: 0 1 auto;
|
||||
height: auto;
|
||||
width: 50%;
|
||||
}
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper .second-column {
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
@media screen and (min-width: 64rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid a.card-wrapper {
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
position: relative;
|
||||
background-color: #fff none repeat scroll 0% 0%;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
-moz-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
box-shadow: rgba(0, 0, 0, 0.09) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.15) 0px 0.25rem 0.5rem 0px;
|
||||
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) 0s;
|
||||
}
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.26) 0px 0.25rem 0.5rem 0px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 75rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
padding: 2rem 2.5rem;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 36rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .logo {
|
||||
margin-right: 0.75rem;
|
||||
width: 1.2rem;
|
||||
min-width: 1.2rem;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content h5 {
|
||||
color: rgb(61, 61, 61);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
color: rgb(92, 92, 92);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content code {
|
||||
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.component-wrapper span.em {
|
||||
color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.component-wrapper a {
|
||||
transition: color 125ms;
|
||||
color: rgb(61, 61, 61);
|
||||
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
|
||||
padding: 2px 6px;
|
||||
margin: 0px 1px;
|
||||
border-radius: 4px;
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.component-wrapper a:hover {
|
||||
color: var(--md-typeset-a-color);
|
||||
background: var(--md-accent-fg-color--transparent);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- Hero for landing page -->
|
||||
<div class="md-container tx-hero">
|
||||
<div class="md-grid md-typeset">
|
||||
<div class="md-main__inner">
|
||||
<div>
|
||||
<h1>A beautiful and simple UI to manage your WireGuard peers and interfaces</h1>
|
||||
<p>WireGuard Portal is an open source web-based user interface that makes it easy to setup and manage
|
||||
WireGuard VPN connections. It's built on top of WireGuard's official <span class="em">wgctrl</span> library.</p>
|
||||
</p>
|
||||
<a
|
||||
href="documentation/overview/"
|
||||
title="Get Started"
|
||||
class="md-button md-button--primary"
|
||||
>
|
||||
Get started
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" style="margin-left:2px"><path d="M1 5.16772H9.5M9.5 5.16772L6.5 1.66772M9.5 5.16772L6.5 8.66772" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-container">
|
||||
<div class="tx-hero__image">
|
||||
<img
|
||||
src="{{config.site_url}}/assets/images/screenshot.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-container secondary-section">
|
||||
<div class="g">
|
||||
<!-- Architecture as building blocks -->
|
||||
<div class="section">
|
||||
<div class="component-wrapper">
|
||||
<div class="first-column">
|
||||
<h3>More information about WireGuard</h3>
|
||||
<p>
|
||||
WireGuard® is an extremely <span class="em">simple</span> yet <span class="em">fast</span> and modern
|
||||
VPN that utilizes <span class="em">state-of-the-art cryptography</span>.
|
||||
</p>
|
||||
<p>
|
||||
WireGuard uses state-of-the-art <a href="https://www.wireguard.com/protocol/">cryptography</a> and still
|
||||
manages to be as easy to configure and deploy as SSH.
|
||||
A combination of extremely high-speed cryptographic primitives and the fact that WireGuard lives inside
|
||||
the Linux kernel means that secure networking can be very high-speed.
|
||||
It is suitable for both small embedded devices like smartphones and fully loaded backbone routers.
|
||||
</p>
|
||||
</div>
|
||||
<div class="second-column">
|
||||
<div class="image-wrapper">
|
||||
<img
|
||||
src="{{config.site_url}}/assets/images/wg-tool.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="component-wrapper" style="display: block;">
|
||||
<h4>Explore official documentation</h4>
|
||||
|
||||
<!-- Arch as code -->
|
||||
<div class="responsive-grid">
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/octicons/file-code-24.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Official Website</h5>
|
||||
<p>
|
||||
If you'd like a general conceptual overview of what WireGuard is about, read onward here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Networking -->
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/protocol/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/fontawesome/solid/network-wired.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Protocol & Cryptography</h5>
|
||||
<p>
|
||||
WireGuard uses state-of-the-art cryptography, like the Noise protocol framework, Curve25519, ChaCha20, Poly1305, BLAKE2, SipHash24, HKDF, and secure trusted constructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Customize -->
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/install/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/fontawesome/solid/puzzle-piece.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Client Installation</h5>
|
||||
<p>
|
||||
You may progress to installation and reading the quickstart instructions on how to use it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Content -->
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- Application footer -->
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
17
docs/theme-overrides/main.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{% if page and page.meta and page.meta.title %}
|
||||
<meta property="og:title" content="{{ page.meta.title }}" />
|
||||
{% endif %}
|
||||
{% if page and page.meta and page.meta.image %}
|
||||
<meta property="og:image" content="{{ page.meta.image }}" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="{{ page.meta.image_width }}" />
|
||||
<meta property="og:image:height" content="{{ page.meta.image_height }}" />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
<meta property="twitter:title" content="{{ page.meta.twitter_title }}" />
|
||||
<meta property="twitter:image" content="{{ page.meta.image }}" />
|
||||
<meta property="twitter:image:alt" content="{{ page.meta.image_alt }}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
32
docs/theme-overrides/partials/footer.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% import "partials/language.html" as lang with context %}
|
||||
|
||||
<!-- Application footer -->
|
||||
<footer class="md-footer">
|
||||
<!-- Further information -->
|
||||
<div class="md-footer-meta md-typeset" style="background-color: #fff;">
|
||||
<div class="md-footer-meta__inner md-grid" style="background-color: #fff;">
|
||||
|
||||
<!-- Copyright and theme information -->
|
||||
<div class="md-footer-copyright">
|
||||
{% if config.copyright %}
|
||||
<div class="md-footer-copyright__highlight" style="color: rgb(38, 38, 38);">
|
||||
{{ config.copyright }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="color: rgb(38, 38, 38);">
|
||||
Made with
|
||||
<a
|
||||
href="https://squidfunk.github.io/mkdocs-material/"
|
||||
target="_blank" rel="noopener"
|
||||
style="color: black;"
|
||||
>
|
||||
Material for MkDocs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social links -->
|
||||
{% include "partials/social.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
12
efs.go
@ -1,12 +0,0 @@
|
||||
package wg_portal
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets/tpl/*
|
||||
var Templates embed.FS
|
||||
|
||||
//go:embed assets/css/*
|
||||
//go:embed assets/fonts/*
|
||||
//go:embed assets/img/*
|
||||
//go:embed assets/js/*
|
||||
var Statics embed.FS
|