Compare commits
256 Commits
v2.0.0-alp
...
doc_improv
Author | SHA1 | Date | |
---|---|---|---|
|
e65cab4857 | ||
|
75ec234a72 | ||
|
4729bccdd3 | ||
|
afb38b685c | ||
|
7cd7d13dc7 | ||
|
d945e313b2 | ||
|
c5fe82ab11 | ||
|
765fb09770 | ||
|
6d2a5fa6de | ||
|
891d499a18 | ||
|
db357b82d0 | ||
|
b61d84ec4f | ||
|
d311313cb4 | ||
|
0cbca61c15 | ||
|
c79a6c83a8 | ||
|
098a9fe23e | ||
|
41cab5f7ea | ||
|
708c558211 | ||
|
99df4ca3cd | ||
|
9884d8c002 | ||
|
b099e8abfa | ||
|
112f6bfb77 | ||
|
a86f83a219 | ||
|
131413b470 | ||
|
2246829151 | ||
|
c20f17cddf | ||
|
3f76aa416f | ||
|
6a8b28df88 | ||
|
ffef1f7b12 | ||
|
dc002b156b | ||
|
1794b8653a | ||
|
a6d985d3ce | ||
|
a7bd3b3f95 | ||
|
f286840964 | ||
|
edb88b5768 | ||
|
588bbca141 | ||
|
f08740991b | ||
|
dd28a8dddf | ||
|
f994700caf | ||
|
be29abd29a | ||
|
94785c10ec | ||
|
3a732fd3e5 | ||
|
f0be66aea4 | ||
|
cbf8c5bca9 | ||
|
b6bfa1f6de | ||
|
0c8d6223ce | ||
|
e3b65ca337 | ||
|
61d8aa6589 | ||
|
7fd2bbad02 | ||
|
75a5f3d815 | ||
|
e9005b1b90 | ||
|
8816165260 | ||
|
ab9995350f | ||
|
7df4e4b813 | ||
|
657c4307b3 | ||
|
b918fb6522 | ||
|
78deede360 | ||
|
a8fb4365cf | ||
|
1394be2341 | ||
|
0102588d23 | ||
|
6a96925be7 | ||
|
f018babca7 | ||
|
c6253e7c15 | ||
|
2a1d82251e | ||
|
99d6ce73ad | ||
|
3eb84f0ee9 | ||
|
d8a57edef9 | ||
|
8271dd7c1f | ||
|
4ca37089bc | ||
|
8e5d5138c0 | ||
|
c73286e11a | ||
|
b4aa6f8ef3 | ||
|
432c627f9b | ||
|
cd60761ea7 | ||
|
2c8304417b | ||
|
020ebb64e7 | ||
|
923d4a6188 | ||
|
2b46dca770 | ||
|
b9c4ca04f5 | ||
|
dddf0c475b | ||
|
fe60a5ab9b | ||
|
e176e07f7d | ||
|
b06c03ef8e | ||
|
6b0b78d749 | ||
|
62f3c8d4a1 | ||
|
fbcb22198c | ||
|
2c443a4a9b | ||
|
059234d416 | ||
|
e2966d32ea | ||
|
9354a1d9d3 | ||
|
e75a32e4d0 | ||
|
1d94f6baaf | ||
|
6681dfa96f | ||
|
a60feb7fc9 | ||
|
37904f96fb | ||
|
1e9ee25e49 | ||
|
30eac7c44a | ||
|
801ce76616 | ||
|
5f9c3bab3e | ||
|
e19f42b1eb | ||
|
34fb373659 | ||
|
b938bc8c4c | ||
|
87bf5da5bd | ||
|
3723e4cc75 | ||
|
6cbccf6d43 | ||
|
a49cfa6343 | ||
|
fe681c015c | ||
|
7d0da4e7ad | ||
|
3218bdd6fb | ||
|
12ccd6e32d | ||
|
02ed7b19df | ||
|
678b6c6456 | ||
|
0206952182 | ||
|
53bae9d194 | ||
|
f616a9f5f4 | ||
|
bf5453c264 | ||
|
fd631d3b9f | ||
|
9680e8350c | ||
|
7473132932 | ||
|
5c51573874 | ||
|
fdb436b135 | ||
|
e24acfa57d | ||
|
10332c7f9a | ||
|
f7d7038829 | ||
|
66ccdc29e9 | ||
|
40b4538e78 | ||
|
986f6fdead | ||
|
dabdf111f9 | ||
|
b074af6dc5 | ||
|
eeb0c87c68 | ||
|
67f076effe | ||
|
f6d7a851d1 | ||
|
fc712ebf42 | ||
|
43163273fa | ||
|
5697c2b7f2 | ||
|
e983a7b8f3 | ||
|
c33eaba1c0 | ||
|
3774257abb | ||
|
588f09bdaa | ||
|
7557a6ef5a | ||
|
3478645317 | ||
|
a950dd76ba | ||
|
8c0ecec485 | ||
|
d01d865b4d | ||
|
1b8cdc3417 | ||
|
d35889de73 | ||
|
0b18b5efd6 | ||
|
2cf2341e4c | ||
|
043d25a08f | ||
|
f6c8cd5ea8 | ||
|
a04eaa4bfb | ||
|
7a0a2117f5 | ||
|
2cea2e477a | ||
|
c2658534b0 | ||
|
2030c59362 | ||
|
e31c170f48 | ||
|
49a987cbce | ||
|
3526240faf | ||
|
075fd0171e | ||
|
c73ce0288e | ||
|
31c0daeba8 | ||
|
662e9c0549 | ||
|
6523a87dfb | ||
|
7ccec5db8d | ||
|
c211c56f75 | ||
|
17844ed929 | ||
|
2d78fe33b8 | ||
|
63d85d8123 | ||
|
26d3257516 | ||
|
d596f578f6 | ||
|
ad267ed0a8 | ||
|
624988aef1 | ||
|
3020fbca4e | ||
|
6d86f15ff8 | ||
|
62dbdfe0f9 | ||
|
378252ba2f | ||
|
0664bd0ad0 | ||
|
877cdae587 | ||
|
edb5c82a66 | ||
|
0ea24e313d | ||
|
983568b36a | ||
|
81ff0cde60 | ||
|
0f27443ffc | ||
|
ca6070689e | ||
|
ba9b6c39e0 | ||
|
afcba8d43e | ||
|
90a570bd66 | ||
|
f7c3bdf456 | ||
|
486a6ac038 | ||
|
bf9183256a | ||
|
6bb683047e | ||
|
5a289276f4 | ||
|
d8eac37302 | ||
|
386597e057 | ||
|
f22a7e4a2e | ||
|
ae1be0e367 | ||
|
7a08c14de4 | ||
|
2c01f42369 | ||
|
3196010a58 | ||
|
6ffe1a90ae | ||
|
e3d05a4678 | ||
|
deff2334ac | ||
|
4f1044a963 | ||
|
2428dedc42 | ||
|
605841f2a0 | ||
|
a46dabc1d3 | ||
|
3f72de6af4 | ||
|
f1f5280cbc | ||
|
48f4b6cb0e | ||
|
58294a3c2a | ||
|
6f52cb2ada | ||
|
85381121ee | ||
|
a6d985c2fe | ||
|
aebf80bf68 | ||
|
e72ba87619 | ||
|
288b7794ca | ||
|
95e10dcc24 | ||
|
c17c182926 | ||
|
d8c1b67a2e | ||
|
c325e4590b | ||
|
a3f5ec1311 | ||
|
2f7819ca9b | ||
|
86fbff886f | ||
|
52c3bc8d92 | ||
|
ea055f3428 | ||
|
1d862c01d5 | ||
|
38310d6ff2 | ||
|
68903597eb | ||
|
2cfd565e3f | ||
|
6f617d6e86 | ||
|
349a6befa1 | ||
|
2de438add8 | ||
|
e565e26c65 | ||
|
acc785e4ca | ||
|
c89f201c78 | ||
|
3279cb2204 | ||
|
6fb6dc0d23 | ||
|
8279aba15e | ||
|
c8989e1ca3 | ||
|
9e1b6b6d91 | ||
|
180b43608d | ||
|
f76b59286e | ||
|
c970b81d84 | ||
|
c37a85fa0b | ||
|
5dcb3eca6d | ||
|
1287215837 | ||
|
26cd286c57 | ||
|
eae1bc765d | ||
|
0ade556e80 | ||
|
1b4b5ff161 | ||
|
81e696fc7d | ||
|
ebe902d119 | ||
|
4a0fcfbf60 | ||
|
1f47075020 | ||
|
faf454f649 | ||
|
35939c92c9 |
@@ -1,67 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
build-latest:
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-latest-v4-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Build Frontend
|
||||
command: |
|
||||
make frontend
|
||||
- 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.21-node
|
||||
|
||||
workflows:
|
||||
build-and-release:
|
||||
jobs:
|
||||
#--------------- BUILD ---------------#
|
||||
- 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/
|
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: h44z # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
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. -->
|
35
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
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@v5
|
||||
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@v6
|
||||
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@v5
|
||||
|
||||
- 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
|
146
.github/workflows/docker-publish.yml
vendored
@@ -1,30 +1,24 @@
|
||||
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]
|
||||
# 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@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -34,106 +28,94 @@ jobs:
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: wgportal/wg-portal
|
||||
images: |
|
||||
wgportal/wg-portal
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
latest=true
|
||||
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={{major}}
|
||||
type=semver,pattern=v{{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
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 }}
|
||||
|
||||
build-github:
|
||||
name: Push Docker image to Github Container Registry
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
- 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 }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
|
||||
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
|
||||
id: get_version
|
||||
for file in binaries/linux*/wg-portal; do
|
||||
mv $file binaries/wg-portal_$(basename $(dirname $file))
|
||||
done
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: binaries
|
||||
path: binaries/wg-portal_linux*
|
||||
retention-days: 10
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
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@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=true
|
||||
prefix=
|
||||
suffix=
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern=v{{major}}
|
||||
name: binaries
|
||||
|
||||
# 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@v5
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
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 }}
|
||||
files: 'wg-portal_linux*'
|
||||
generate_release_notes: true
|
||||
|
40
.github/workflows/pages.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
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
|
||||
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||
run: mike deploy --push ${{ github.ref_name }}
|
||||
env:
|
||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Publish latest documentation
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
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"
|
13
.gitignore
vendored
@@ -32,7 +32,12 @@ ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
go.sum
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
||||
/config.yml
|
||||
/config.yaml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
# ignore local frontend dist directory
|
||||
internal/app/api/core/frontend-dist
|
||||
# mkdocs output directory
|
||||
site/
|
||||
|
93
Dockerfile
@@ -1,56 +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.21 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.25-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.22
|
||||
# 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
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# by default, the web-portal is reachable on port 8888
|
||||
# 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
|
||||
ENTRYPOINT [ "/app/wg-portal" ]
|
||||
ENTRYPOINT [ "/app/wg-portal" ]
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020-2023 Christoph Haas
|
||||
Copyright (c) 2020-2025 Christoph Haas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
13
Makefile
@@ -127,4 +127,15 @@ 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
|
||||
-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
|
||||
|
237
README.md
@@ -1,208 +1,79 @@
|
||||
# WireGuard Portal (v2 - testing)
|
||||
# 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/wgportal/wg-portal/)
|
||||
|
||||
> :warning: **IMPORTANT** Version 2 is currently under development and may contain bugs. It is currently not advised to use this version
|
||||
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||
|
||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
|
||||
Please update the Docker image from **h44z/wg-portal** to **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 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.
|
||||
|
||||
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 - the whole application is a single binary
|
||||
* Responsive web UI written in Vue.JS
|
||||
* 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 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
|
||||
* Handle route and DNS settings like wg-quick does
|
||||
* ~~REST API for management and client deployment~~ (coming soon)
|
||||
|
||||

|
||||
* Self-hosted - the whole application is a single binary
|
||||
* Responsive multi-language web UI with dark-mode 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), Passkey support
|
||||
* IPv6 ready
|
||||
* Docker ready
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* Supports multiple WireGuard backends (wgctrl or MikroTik)
|
||||
* 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
|
||||
|
||||
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
||||

|
||||
|
||||
## Configuration
|
||||
You can configure WireGuard Portal using a yaml configuration file.
|
||||
The filepath of the yaml configuration file defaults to **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=/home/test/config.yml ./wg-portal-amd64`.
|
||||
## Documentation
|
||||
|
||||
By default, WireGuard Portal uses a SQLite database. The database is stored in **data/sqlite.db** in the working directory of the executable.
|
||||
|
||||
### Configuration Options
|
||||
The following configuration options are available:
|
||||
|
||||
| configuration key | parent key | default_value | description |
|
||||
|---------------------------|------------|--------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| admin_user | core | admin@wgportal.local | The administrator user. This user will be created as default admin if it does not yet exist. |
|
||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
||||
| create_default_peer | 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_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
||||
| log_level | advanced | warn | The loglevel, can be one of: trace, debug, info, warn, error. |
|
||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
||||
| log_json | advanced | false | Logs in JSON format. |
|
||||
| ldap_sync_interval | advanced | 15m | The time interval after which users will be synchronized from LDAP. |
|
||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
||||
| data_collection_interval | statistics | 10m | The interval between the data collection cycles. |
|
||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
||||
| port | mail | 25 | The mail-server SMTP port. |
|
||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
||||
| username | mail | | The SMTP user name. |
|
||||
| password | mail | | The SMTP password. |
|
||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
||||
| callback_url_prefix | auth | /api/v0 | OAuth callback URL prefix. The full callback URL will look like: https://wg.portal.local/callback_url_prefix/provider_name/callback |
|
||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||
| client_id | auth/oidc | | The OAuth client id. |
|
||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
||||
| base_url | auth/oauth | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
||||
| client_id | auth/oauth | | The OAuth client id. |
|
||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
||||
| redirect_url | auth/oauth | | The redirect URL. |
|
||||
| user_info_url | auth/oauth | | The URL for the user information endpoint. |
|
||||
| scopes | auth/oauth | | OAuth scopes. |
|
||||
| field_map | auth/oauth | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
||||
| registration_enabled | auth/oauth | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| url | auth/ldap | | The LDAP server url. For example: ldap://srv-ad01.company.local:389 |
|
||||
| start_tls | auth/ldap | | Use STARTTLS to encrypt LDAP requests. |
|
||||
| cert_validation | auth/ldap | | Validate the LDAP server certificate. |
|
||||
| tls_certificate_path | auth/ldap | | A path to the TLS certificate. |
|
||||
| tls_key_path | auth/ldap | | A path to the TLS key. |
|
||||
| base_dn | auth/ldap | | The base DN for searching users. For example: DC=COMPANY,DC=LOCAL |
|
||||
| bind_user | auth/ldap | | The bind user. For example: company\\ldap_wireguard |
|
||||
| bind_pass | auth/ldap | | The bind password. |
|
||||
| field_map | auth/ldap | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and memberof. |
|
||||
| login_filter | auth/ldap | | LDAP filters for users that should be allowed to log in. {{login_identifier}} will be replaced with the login username. |
|
||||
| admin_group | auth/ldap | | Users in this group are marked as administrators. |
|
||||
| synchronize | auth/ldap | | Periodically synchronize users (name, department, phone, status, ...) to the WireGuard Portal database. |
|
||||
| disable_missing | auth/ldap | | If synchronization is enabled, missing LDAP users will be disabled in WireGuard Portal. |
|
||||
| sync_filter | auth/ldap | | LDAP filters for users that should be synchronized to WireGuard Portal. |
|
||||
| registration_enabled | auth/ldap | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
||||
| debug | database | false | Debug database statements (log each statement). |
|
||||
| slow_query_threshold | database | | A threshold for slow database queries. If the threshold is exceeded, a warning message will be logged. |
|
||||
| type | database | sqlite | The database type. Allowed values: sqlite, mssql, mysql or postgres. |
|
||||
| dsn | database | data/sqlite.db | The database DSN. For example: user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local |
|
||||
| request_logging | web | false | Log all HTTP requests. |
|
||||
| external_url | web | http://localhost:8888 | The URL where a client can access WireGuard Portal. |
|
||||
| listening_address | web | :8888 | The listening port of the web server. |
|
||||
| session_identifier | web | wgPortalSession | The session identifier for the web frontend. |
|
||||
| session_secret | web | very_secret | The session secret for the web frontend. |
|
||||
| csrf_secret | web | extremely_secret | The CSRF secret. |
|
||||
| site_title | web | WireGuard Portal | The title that is shown in the web frontend. |
|
||||
| site_company_name | web | WireGuard Portal | The company name that is shown at the bottom of the web frontend. |
|
||||
|
||||
|
||||
## Upgrading from V1
|
||||
|
||||
> :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.yml) 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 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 config.yml.
|
||||
Ensure that the new database does not contain any data!
|
||||
|
||||
|
||||
## V2 TODOs
|
||||
* Public REST API
|
||||
* Translations
|
||||
* Documentation
|
||||
* Audit UI
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version 1.20 or higher has to be installed to build WireGuard Portal.
|
||||
If you want to re-compile the frontend, NodeJS 18 and NPM >= 9 is required.
|
||||
|
||||
```shell
|
||||
# build the frontend (optional)
|
||||
make frontend
|
||||
|
||||
# build the binary
|
||||
make build
|
||||
```
|
||||
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||
|
||||
## What is out of scope
|
||||
* 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.
|
||||
|
||||
* 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
|
||||
|
||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||
* [Vue.JS](https://vuejs.org/), for the frontend
|
||||
|
||||
* [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>
|
||||
|
||||
## Contributors and Sponsors
|
||||
|
||||
Thanks so much for all your contributions! They’re truly appreciated and help keep WireGuard Portal moving ahead.
|
||||
|
||||
<a href="https://github.com/h44z/wg-portal/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=h44z/wg-portal" />
|
||||
</a>
|
||||
|
||||
Want to support the project? You can buy me a coffee or join as a contributor - every bit of support helps!
|
||||
[Become a sponsor!](https://github.com/sponsors/h44z)
|
||||
|
||||
|
||||
> [!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 | :x: |
|
||||
|
||||
## 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!
|
@@ -7,11 +7,15 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"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
|
||||
@@ -19,10 +23,9 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
apiBasePath := filepath.Join(wd, "/internal/app/api")
|
||||
apis := []string{"v0"}
|
||||
apiBasePath := filepath.Join(wd, apiRootPath)
|
||||
apis := []string{"v0", "v1"}
|
||||
|
||||
hasError := false
|
||||
for _, apiVersion := range apis {
|
||||
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
|
||||
|
||||
@@ -33,16 +36,20 @@ func main() {
|
||||
|
||||
err := generateApi(apiBasePath, apiPath, apiVersion)
|
||||
if err != nil {
|
||||
hasError = true
|
||||
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
|
||||
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)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func generateApi(basePath, apiPath, version string) error {
|
||||
@@ -51,10 +58,10 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
Excludes: "",
|
||||
MainAPIFile: "base.go",
|
||||
PropNamingStrategy: swag.PascalCase,
|
||||
OutputDir: filepath.Join(basePath, "core/assets/doc"),
|
||||
OutputDir: filepath.Join(basePath, apiDocPath),
|
||||
OutputTypes: []string{"json", "yaml"},
|
||||
ParseVendor: false,
|
||||
ParseDependency: true,
|
||||
ParseDependency: 3,
|
||||
MarkdownFilesDir: "",
|
||||
ParseInternal: true,
|
||||
GeneratedTime: false,
|
||||
@@ -68,3 +75,43 @@ func generateApi(basePath, apiPath, version string) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -2,81 +2,104 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
evbus "github.com/vardius/message-bus"
|
||||
)
|
||||
|
||||
// main entry point for WireGuard Portal
|
||||
func main() {
|
||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
logrus.Infof("Starting WireGuard Portal V2...")
|
||||
logrus.Infof("WireGuard Portal version: %s", internal.Version)
|
||||
slog.Info("Starting WireGuard Portal V2...", "version", internal.Version)
|
||||
|
||||
cfg, err := config.GetConfig()
|
||||
internal.AssertNoError(err)
|
||||
setupLogging(cfg)
|
||||
internal.SetupLogging(cfg.Advanced.LogLevel, cfg.Advanced.LogPretty, cfg.Advanced.LogJson)
|
||||
|
||||
cfg.LogStartupValues()
|
||||
|
||||
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
|
||||
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
database, err := adapters.NewSqlRepository(rawDb)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
wireGuard := adapters.NewWireGuardRepository()
|
||||
wireGuard, err := wireguard.NewControllerManager(cfg)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
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(cfg, rawDb)
|
||||
shouldExit, err := app.HandleProgramArgs(rawDb)
|
||||
switch {
|
||||
case shouldExit && err == nil:
|
||||
return
|
||||
case shouldExit && err != nil:
|
||||
logrus.Errorf("Failed to process program args: %v", err)
|
||||
case shouldExit:
|
||||
slog.Error("Failed to process program args", "error", err)
|
||||
os.Exit(1)
|
||||
case !shouldExit:
|
||||
default:
|
||||
internal.AssertNoError(err)
|
||||
}
|
||||
|
||||
queueSize := 100
|
||||
eventBus := evbus.New(queueSize)
|
||||
|
||||
auditManager := audit.NewManager(database)
|
||||
|
||||
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
||||
internal.AssertNoError(err)
|
||||
auditRecorder.StartBackgroundJobs(ctx)
|
||||
|
||||
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
||||
internal.AssertNoError(err)
|
||||
userManager.StartBackgroundJobs(ctx)
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager)
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||
internal.AssertNoError(err)
|
||||
authenticator.StartBackgroundJobs(ctx)
|
||||
|
||||
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, 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, database, wireGuard)
|
||||
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)
|
||||
@@ -84,62 +107,89 @@ func main() {
|
||||
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
||||
internal.AssertNoError(err)
|
||||
auditRecorder.StartBackgroundJobs(ctx)
|
||||
|
||||
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
||||
internal.AssertNoError(err)
|
||||
routeManager.StartBackgroundJobs(ctx)
|
||||
|
||||
backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager,
|
||||
statisticsCollector, cfgFileManager, mailManager)
|
||||
webhookManager, err := webhooks.NewManager(cfg, eventBus)
|
||||
internal.AssertNoError(err)
|
||||
err = backend.Startup(ctx)
|
||||
webhookManager.StartBackgroundJobs(ctx)
|
||||
|
||||
err = app.Initialize(cfg, wireGuardManager, userManager)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
||||
validatorManager := validator.New()
|
||||
|
||||
webSrv, err := core.NewServer(cfg, apiFrontend)
|
||||
// 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,
|
||||
webAuthn)
|
||||
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, wireGuard)
|
||||
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()
|
||||
|
||||
logrus.Infof("Stopping WireGuard Portal")
|
||||
slog.Info("Stopping WireGuard Portal")
|
||||
|
||||
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
||||
|
||||
logrus.Infof("Stopped WireGuard Portal")
|
||||
}
|
||||
|
||||
func setupLogging(cfg *config.Config) {
|
||||
switch strings.ToLower(cfg.Advanced.LogLevel) {
|
||||
case "trace":
|
||||
logrus.SetLevel(logrus.TraceLevel)
|
||||
case "debug":
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
case "info", "information":
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
case "warn", "warning":
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
case "error":
|
||||
logrus.SetLevel(logrus.ErrorLevel)
|
||||
default:
|
||||
logrus.SetLevel(logrus.WarnLevel)
|
||||
}
|
||||
|
||||
switch {
|
||||
case cfg.Advanced.LogJson:
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{
|
||||
PrettyPrint: cfg.Advanced.LogPretty,
|
||||
})
|
||||
case cfg.Advanced.LogPretty:
|
||||
logrus.SetFormatter(&logrus.TextFormatter{
|
||||
ForceColors: true,
|
||||
DisableColors: false,
|
||||
})
|
||||
}
|
||||
slog.Info("Stopped WireGuard Portal")
|
||||
}
|
||||
|
@@ -1,27 +1,34 @@
|
||||
# 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:
|
||||
callback_url_prefix: http://localhost:8888/api/v0
|
||||
ldap:
|
||||
- id: ldap1
|
||||
provider_name: company ldap
|
||||
display_name: Login with</br>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
|
||||
synchronize: false
|
||||
sync_interval: 0 # sync disabled
|
||||
sync_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
registration_enabled: true
|
||||
oidc:
|
||||
@@ -44,4 +51,46 @@ auth:
|
||||
extra_scopes:
|
||||
- https://www.googleapis.com/auth/userinfo.email
|
||||
- https://www.googleapis.com/auth/userinfo.profile
|
||||
registration_enabled: true
|
||||
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,5 +1,4 @@
|
||||
---
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
@@ -11,8 +10,10 @@ 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
|
||||
- ./config:/app/config
|
||||
|
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 |
BIN
docs/assets/images/interface_view.png
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
docs/assets/images/landing_page.png
Normal file
After Width: | Height: | Size: 110 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/passkey_setup.png
Normal file
After Width: | Height: | Size: 106 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 |
BIN
docs/assets/images/wgportal_dark.png
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
docs/assets/images/wgportal_light.png
Normal file
After Width: | Height: | Size: 129 KiB |
217
docs/documentation/configuration/examples.md
Normal file
@@ -0,0 +1,217 @@
|
||||
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
|
||||
|
||||
backend:
|
||||
# default backend decides where new interfaces are created
|
||||
default: mikrotik
|
||||
|
||||
mikrotik:
|
||||
- id: mikrotik # unique id, not "local"
|
||||
display_name: RouterOS RB5009 # optional nice name
|
||||
api_url: https://10.10.10.10/rest
|
||||
api_user: wgportal
|
||||
api_password: a-super-secret-password
|
||||
api_verify_tls: false # set to false only if using self-signed during testing
|
||||
api_timeout: 30s # maximum request duration
|
||||
concurrency: 5 # limit parallel REST calls to device
|
||||
debug: false # verbose logging for this backend
|
||||
ignored_interfaces: # ignore these interfaces during import
|
||||
- wgTest1
|
||||
- wgTest2
|
||||
|
||||
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
|
||||
|
||||
auth:
|
||||
webauthn:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## 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.
|
||||
# Only users with an @outlook.com email address are allowed to register or login.
|
||||
- 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}"
|
||||
allowed_domains:
|
||||
- "outlook.com"
|
||||
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
|
||||
```
|
||||
|
||||
For more information, check out the usage documentation (e.g. [General Configuration](../usage/general.md) or [Backends Configuration](../usage/backends.md)).
|
766
docs/documentation/configuration/overview.md
Normal file
@@ -0,0 +1,766 @@
|
||||
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-default
|
||||
admin_api_token: ""
|
||||
disable_admin_user: false
|
||||
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
|
||||
|
||||
backend:
|
||||
default: local
|
||||
|
||||
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
|
||||
limit_additional_user_peers: 0
|
||||
|
||||
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: []
|
||||
webauthn:
|
||||
enabled: true
|
||||
min_password_length: 16
|
||||
hide_login_form: false
|
||||
|
||||
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),
|
||||
[`backend`](#backend),
|
||||
[`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-default`
|
||||
- **Description:** The administrator password. The default password should be changed immediately!
|
||||
- **Important:** The password should be strong and secure. The minimum password length is specified in [auth.min_password_length](#min_password_length). By default, it is 16 characters.
|
||||
|
||||
### `disable_admin_user`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, no admin user is created. This is useful if you plan to manage users exclusively through external authentication providers such as LDAP or OAuth.
|
||||
|
||||
### `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.
|
||||
|
||||
---
|
||||
|
||||
## Backend
|
||||
|
||||
Configuration options for the WireGuard backend, which manages the WireGuard interfaces and peers.
|
||||
The current MikroTik backend is in **BETA** and may not support all features.
|
||||
|
||||
### `default`
|
||||
- **Default:** `local`
|
||||
- **Description:** The default backend to use for managing WireGuard interfaces.
|
||||
Valid options are: `local`, or other backend id's configured in the `mikrotik` section.
|
||||
|
||||
### `ignored_local_interfaces`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of interface names to exclude when enumerating local interfaces.
|
||||
This is useful if you want to prevent certain interfaces from being imported from the local system.
|
||||
|
||||
### Mikrotik
|
||||
|
||||
The `mikrotik` array contains a list of MikroTik backend definitions. Each entry describes how to connect to a MikroTik RouterOS instance that hosts WireGuard interfaces.
|
||||
|
||||
Below are the properties for each entry inside `backend.mikrotik`:
|
||||
|
||||
#### `id`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A unique identifier for this backend.
|
||||
This value can be referenced by `backend.default` to use this backend as default.
|
||||
The identifier must be unique across all backends and must not use the reserved keyword `local`.
|
||||
|
||||
#### `display_name`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A human-friendly display name for this backend. If omitted, the `id` will be used as the display name.
|
||||
|
||||
#### `api_url`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Base URL of the MikroTik REST API, including scheme and path, e.g., `https://10.10.10.10:8729/rest`.
|
||||
|
||||
#### `api_user`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Username for authenticating against the MikroTik API.
|
||||
Ensure that the user has sufficient permissions to manage WireGuard interfaces and peers.
|
||||
|
||||
#### `api_password`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** Password for the specified API user.
|
||||
|
||||
#### `api_verify_tls`
|
||||
- **Default:** `false`
|
||||
- **Description:** Whether to verify the TLS certificate of the MikroTik API endpoint. Set to `false` to allow self-signed certificates (not recommended for production).
|
||||
|
||||
#### `api_timeout`
|
||||
- **Default:** `30s`
|
||||
- **Description:** Timeout for API requests to the MikroTik device. Uses Go duration format (e.g., `10s`, `1m`). If omitted, a default of 30 seconds is used.
|
||||
|
||||
#### `concurrency`
|
||||
- **Default:** `5`
|
||||
- **Description:** Maximum number of concurrent API requests the backend will issue when enumerating interfaces and their details. If `0` or negative, a sane default of `5` is used.
|
||||
|
||||
#### `ignored_interfaces`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of interface names to exclude during interface enumeration.
|
||||
This is useful if you want to prevent specific interfaces from being imported from the MikroTik device.
|
||||
|
||||
#### `debug`
|
||||
- **Default:** `false`
|
||||
- **Description:** Enable verbose debug logging for the MikroTik backend.
|
||||
|
||||
For more details on configuring the MikroTik backend, see the [Backends](../usage/backends.md) documentation.
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
|
||||
### `limit_additional_user_peers`
|
||||
- **Default:** `0`
|
||||
- **Description:** Limit additional peers a normal user can create. `0` means unlimited.
|
||||
|
||||
---
|
||||
|
||||
## 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`), **Passkeys** (`webauthn`) and **LDAP** (`ldap`).
|
||||
Each can have multiple providers configured. Below are the relevant keys.
|
||||
|
||||
Some core authentication options are shared across all providers, while others are specific to each provider type.
|
||||
|
||||
### `min_password_length`
|
||||
- **Default:** `16`
|
||||
- **Description:** Minimum password length for local authentication. This is not enforced for LDAP authentication.
|
||||
The default admin password strength is also enforced by this setting.
|
||||
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
|
||||
|
||||
### `hide_login_form`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, the login form is hidden and only the OIDC, OAuth, LDAP, or WebAuthn providers are shown. This is useful if you want to enforce a specific authentication method.
|
||||
If no social login providers are configured, the login form is always shown, regardless of this setting.
|
||||
- **Important:** You can still access the login form by adding the `?all` query parameter to the login URL (e.g. https://wg.portal/#/login?all).
|
||||
|
||||
---
|
||||
|
||||
### 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`).
|
||||
|
||||
#### `allowed_domains`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||
|
||||
#### `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.
|
||||
|
||||
#### `allowed_domains`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||
|
||||
#### `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))
|
||||
```
|
||||
- **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user.
|
||||
If the filter returns multiple or no users, the login will fail.
|
||||
|
||||
#### `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.
|
||||
|
||||
---
|
||||
|
||||
### WebAuthn (Passkeys)
|
||||
|
||||
The `webauthn` section contains configuration options for WebAuthn authentication (passkeys).
|
||||
|
||||
#### `enabled`
|
||||
- **Default:** `true`
|
||||
- **Description:** If `true`, Passkey authentication is enabled. If `false`, WebAuthn is disabled.
|
||||
Users are encouraged to use Passkeys for secure authentication instead of passwords.
|
||||
If a passkey is registered, the password login is still available as a fallback. Ensure that the password is strong and secure.
|
||||
|
||||
## 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.
|
||||
Further details can be found in the [usage documentation](../usage/webhooks.md).
|
||||
|
||||
### `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).
|
||||
|
161
docs/documentation/getting-started/docker.md
Normal file
@@ -0,0 +1,161 @@
|
||||
## 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).
|
||||
|
||||
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
||||
|
||||
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. For production deployments of WireGuard Portal, we strongly recommend using one of these versioned tags instead of the latest or canary tags.
|
||||
|
||||
There are different types of these tags:
|
||||
|
||||
- Major version tags: `v2` or `2`. These tags always refer to the latest image for WireGuard Portal version **2**.
|
||||
- Minor version tags: `v2.x` or `2.0`. These tags always refer to the latest image for WireGuard Portal version **2.x**.
|
||||
- Specific version tags (patch version): `v2.0.0` or `2.0.0`. These tags denote a very specific release. 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 for a specific version show up in the Docker repository, they will never change.
|
||||
|
||||
#### The `latest` tag
|
||||
|
||||
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
|
||||
|
||||
#### The `master` tag
|
||||
|
||||
This is the most recent build to the main branch! It changes a lot and is very unstable.
|
||||
|
||||
We recommend that you don't use it except for development purposes or to test the latest features.
|
||||
|
||||
|
||||
## 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"/>
|
1565
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"]
|
||||
```
|
57
docs/documentation/usage/backends.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Backends
|
||||
|
||||
WireGuard Portal can manage WireGuard interfaces and peers on different backends.
|
||||
Each backend represents a system where interfaces actually live.
|
||||
You can register multiple backends and choose which one to use per interface.
|
||||
A global default backend determines where newly created interfaces go (unless you explicitly choose another in the UI).
|
||||
|
||||
**Supported backends:**
|
||||
- **Local** (default): Manages interfaces on the host running WireGuard Portal (Linux WireGuard via wgctrl). Use this when the portal should directly configure wg devices on the same server.
|
||||
- **MikroTik** RouterOS (_beta_): Manages interfaces and peers on MikroTik devices via the RouterOS REST API. Use this to control WG interfaces on RouterOS v7+.
|
||||
|
||||
How backend selection works:
|
||||
- The default backend is configured at `backend.default` (_local_ or the id of a defined MikroTik backend).
|
||||
New interfaces created in the UI will use this backend by default.
|
||||
- Each interface stores its backend. You can select a different backend when creating a new interface.
|
||||
|
||||
## Configuring MikroTik backends (RouterOS v7+)
|
||||
|
||||
> :warning: The MikroTik backend is currently marked beta. While basic functionality is implemented, some advanced features are not yet implemented or contain bugs. Please test carefully before using in production.
|
||||
|
||||
The MikroTik backend uses the [REST API](https://help.mikrotik.com/docs/spaces/ROS/pages/47579162/REST+API) under a base URL ending with /rest.
|
||||
You can register one or more MikroTik devices as backends for a single WireGuard Portal instance.
|
||||
|
||||
### Prerequisites on MikroTik:
|
||||
- RouterOS v7 with WireGuard support.
|
||||
- REST API enabled and reachable over HTTP(S). A typical base URL is https://<router-address>:8729/rest or https://<router-address>/rest depending on your service setup.
|
||||
- A dedicated RouterOS user with the following group permissions:
|
||||
- **api** (for logging in via REST API)
|
||||
- **rest-api** (for logging in via REST API)
|
||||
- **read** (to read interface and peer data)
|
||||
- **write** (to create/update interfaces and peers)
|
||||
- **test** (to perform ping checks)
|
||||
- **sensitive** (to read private keys)
|
||||
- TLS certificate on the device is recommended. If you use a self-signed certificate during testing, set `api_verify_tls`: _false_ in wg-portal (not recommended for production).
|
||||
|
||||
Example WireGuard Portal configuration (config/config.yaml):
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
# default backend decides where new interfaces are created
|
||||
default: mikrotik-prod
|
||||
|
||||
mikrotik:
|
||||
- id: mikrotik-prod # unique id, not "local"
|
||||
display_name: RouterOS RB5009 # optional nice name
|
||||
api_url: https://10.10.10.10/rest
|
||||
api_user: wgportal
|
||||
api_password: a-super-secret-password
|
||||
api_verify_tls: true # set to false only if using self-signed during testing
|
||||
api_timeout: 30s # maximum request duration
|
||||
concurrency: 5 # limit parallel REST calls to device
|
||||
debug: false # verbose logging for this backend
|
||||
```
|
||||
|
||||
### Known limitations:
|
||||
- The MikroTik backend is still in beta. Some features may not work as expected.
|
||||
- Not all WireGuard Portal features are supported yet (e.g., no support for interface hooks)
|
57
docs/documentation/usage/general.md
Normal file
@@ -0,0 +1,57 @@
|
||||
This documentation section describes the general usage of WireGuard Portal.
|
||||
If you are looking for specific setup instructions, please refer to the *Getting Started* and [*Configuration*](../configuration/overview.md) sections,
|
||||
for example, using a [Docker](../getting-started/docker.md) deployment.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
WireGuard Portal is a web-based configuration portal for WireGuard server management. It allows managing multiple WireGuard interfaces and users from a single web UI.
|
||||
WireGuard Interfaces can be categorized into three types:
|
||||
|
||||
- **Server**: A WireGuard server interface that to which multiple peers can connect. In this mode, it is possible to specify default settings for all peers, such as the IP address range, DNS servers, and MTU size.
|
||||
- **Client**: A WireGuard client interface that can be used to connect to a WireGuard server. Usually, such an interface has exactly one peer.
|
||||
- **Unknown**: This is the default type for imported interfaces. It is encouraged to change the type to either `Server` or `Client` after importing the interface.
|
||||
|
||||
## Accessing the Web UI
|
||||
|
||||
The web UI should be accessed via the URL specified in the `external_url` property of the configuration file.
|
||||
By default, WireGuard Portal listens on port `8888` for HTTP connections. Check the [Security](security.md) section for more information on securing the web UI.
|
||||
|
||||
So the default URL to access the web UI is:
|
||||
|
||||
```
|
||||
http://localhost:8888
|
||||
```
|
||||
|
||||
A freshly set-up WireGuard Portal instance will have a default admin user with the username `admin@wgportal.local` and the password `wgportal-default`.
|
||||
You can and should override the default credentials in the configuration file. Make sure to change the default password immediately after the first login!
|
||||
|
||||
|
||||
### Basic UI Description
|
||||
|
||||

|
||||
|
||||
As seen in the screenshot above, the web UI is divided into several sections which are accessible via the navigation bar on the top of the screen.
|
||||
|
||||
1. **Home**: The landing page of WireGuard Portal. It provides a staring point for the user to access the different sections of the web UI. It also provides quick links to WireGuard Client downloads or official documentation.
|
||||
2. **Interfaces**: This section allows you to manage the WireGuard interfaces. You can add, edit, or delete interfaces, as well as view their status and statistics. Peers for each interface can be managed here as well.
|
||||
3. **Users**: This section allows you to manage the users of WireGuard Portal. You can add, edit, or delete users, as well as view their status and statistics.
|
||||
4. **Key Generator**: This section allows you to generate WireGuard keys locally on your browser. The generated keys are never sent to the server. This is useful if you want to generate keys for a new peer without having to store the private keys in the database.
|
||||
5. **Profile / Settings**: This section allows you to access your own profile page, settings, and audit logs.
|
||||
|
||||
|
||||
### Interface View
|
||||
|
||||

|
||||
|
||||
The interface view provides an overview of the WireGuard interfaces and peers configured in WireGuard Portal.
|
||||
|
||||
The most important elements are:
|
||||
|
||||
1. **Interface Selector**: This dropdown allows you to select the WireGuard interface you want to manage.
|
||||
All further actions will be performed on the selected interface.
|
||||
2. **Create new Interface**: This button allows you to create a new WireGuard interface.
|
||||
3. **Interface Overview**: This section provides an overview of the selected WireGuard interface. It shows the interface type, number of peers, and other important information.
|
||||
4. **List of Peers**: This section provides a list of all peers associated with the selected WireGuard interface. You can view, add, edit, or delete peers from this list.
|
||||
5. **Add new Peer**: This button allows you to add a new peer to the selected WireGuard interface.
|
||||
6. **Add multiple Peers**: This button allows you to add multiple peers to the selected WireGuard interface.
|
||||
This is useful if you want to add a large number of peers at once.
|
37
docs/documentation/usage/ldap.md
Normal file
@@ -0,0 +1,37 @@
|
||||
WireGuard Portal lets you hook up any LDAP server such as Active Directory or OpenLDAP for both authentication and user sync.
|
||||
You can even register multiple LDAP servers side-by-side. When someone logs in via LDAP, their specific provider is remembered,
|
||||
so there's no risk of cross-provider conflicts. Details on the log-in process can be found in the [Security](security.md#ldap-authentication) documentation.
|
||||
|
||||
If you enable LDAP synchronization, all users within the LDAP directory will be created automatically in the WireGuard Portal database if they do not exist.
|
||||
If a user is disabled or deleted in LDAP, the user will be disabled in WireGuard Portal as well.
|
||||
The synchronization process can be fine-tuned by multiple parameters, which are described below.
|
||||
|
||||
## LDAP Synchronization
|
||||
|
||||
WireGuard Portal can automatically synchronize users from LDAP to the database.
|
||||
To enable this feature, set the `sync_interval` property in the LDAP provider configuration to a value greater than "0".
|
||||
The value is a string representing a duration, such as "15m" for 15 minutes or "1h" for 1 hour (check the [exact format definition](https://pkg.go.dev/time#ParseDuration) for details).
|
||||
The synchronization process will run in the background and synchronize users from LDAP to the database at the specified interval.
|
||||
Also make sure that the `sync_filter` property is a well-formed LDAP filter, or synchronization will fail.
|
||||
|
||||
### Limiting Synchronization to Specific Users
|
||||
|
||||
Use the `sync_filter` property in your LDAP provider block to restrict which users get synchronized.
|
||||
It accepts any valid LDAP search filter, only entries matching that filter will be pulled into the portal's database.
|
||||
|
||||
For example, to import only users with a `mail` attribute:
|
||||
```yaml
|
||||
auth:
|
||||
ldap:
|
||||
- id: ldap
|
||||
# ... other settings
|
||||
sync_filter: (mail=*)
|
||||
```
|
||||
|
||||
### Disable Missing Users
|
||||
|
||||
If you set the `disable_missing` property to `true`, any user that is not found in LDAP during synchronization will be disabled in WireGuard Portal.
|
||||
All peers associated with that user will also be disabled.
|
||||
|
||||
If you want a user and its peers to be automatically re-enabled once they are found in LDAP again, set the `auto_re_enable` property to `true`.
|
||||
This will only re-enable the user if they where disabled by the synchronization process. Manually disabled users will not be re-enabled.
|
160
docs/documentation/usage/security.md
Normal file
@@ -0,0 +1,160 @@
|
||||
This section describes the security features available to administrators for hardening WireGuard Portal and protecting its data.
|
||||
|
||||
## Authentication
|
||||
|
||||
WireGuard Portal supports multiple authentication methods, including:
|
||||
|
||||
- Local user accounts
|
||||
- LDAP authentication
|
||||
- OAuth and OIDC authentication
|
||||
- Passkey authentication (WebAuthn)
|
||||
|
||||
Users can have two roles which limit their permissions in WireGuard Portal:
|
||||
|
||||
- **User**: Can manage their own account and peers.
|
||||
- **Admin**: Can manage all users and peers, including the ability to manage WireGuard interfaces.
|
||||
|
||||
### Password Security
|
||||
|
||||
WireGuard Portal supports username and password authentication for both local and LDAP-backed accounts.
|
||||
Local users are stored in the database, while LDAP users are authenticated against an external LDAP server.
|
||||
|
||||
On initial startup, WireGuard Portal automatically creates a local admin account with the password `wgportal-default`.
|
||||
> :warning: This password must be changed immediately after the first login.
|
||||
|
||||
The minimum password length for all local users can be configured in the [`auth`](../configuration/overview.md#auth)
|
||||
section of the configuration file. The default value is **16** characters, see [`min_password_length`](../configuration/overview.md#min_password_length).
|
||||
The minimum password length is also enforced for the default admin user.
|
||||
|
||||
|
||||
### Passkey (WebAuthn) Authentication
|
||||
|
||||
Besides the standard authentication mechanisms, WireGuard Portal supports Passkey authentication.
|
||||
This feature is enabled by default and can be configured in the [`webauthn`](../configuration/overview.md#webauthn-passkeys) section of the configuration file.
|
||||
|
||||
Users can register multiple Passkeys to their account. These Passkeys can be used to log in to the web UI as long as the user is not locked.
|
||||
> :warning: Passkey authentication does not disable password authentication. The password can still be used to log in (e.g., as a fallback).
|
||||
|
||||
To register a Passkey, open the settings page *(1)* in the web UI and click on the "Register Passkey" *(2)* button.
|
||||
|
||||

|
||||
|
||||
|
||||
### OAuth and OIDC Authentication
|
||||
|
||||
WireGuard Portal supports OAuth and OIDC authentication. You can use any OAuth or OIDC provider that supports the authorization code flow,
|
||||
such as Google, GitHub, or Keycloak.
|
||||
|
||||
For OAuth or OIDC to work, you need to configure the [`external_url`](../configuration/overview.md#external_url) property in the [`web`](../configuration/overview.md#web) section of the configuration file.
|
||||
If you are planning to expose the portal to the internet, make sure that the `external_url` is configured to use HTTPS.
|
||||
|
||||
To add OIDC or OAuth authentication to WireGuard Portal, create a Client-ID and Client-Secret in your OAuth provider and
|
||||
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
|
||||
|
||||
#### Limiting Login to Specific Domains
|
||||
|
||||
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth or OIDC providers.
|
||||
This property is a comma-separated list of domains that are allowed to log in. The user's email address is checked against this list.
|
||||
For example, if you want to allow only users with an email address ending in `outlook.com` to log in, set the property as follows:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
oidc:
|
||||
- provider_name: "oidc1"
|
||||
# ... other settings
|
||||
allowed_domains:
|
||||
- "outlook.com"
|
||||
```
|
||||
|
||||
#### Limit Login to Existing Users
|
||||
|
||||
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth or OIDC providers.
|
||||
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||
|
||||
#### Admin Mapping
|
||||
|
||||
You can map users to admin roles based on their attributes in the OAuth or OIDC provider. To do this, set the `admin_mapping` property for the provider.
|
||||
Administrative access can either be mapped by a specific attribute or by group membership.
|
||||
|
||||
**Attribute specific mapping** can be achieved by setting the `admin_value_regex` and the `is_admin` property.
|
||||
The `admin_value_regex` property is a regular expression that is matched against the value of the `is_admin` attribute.
|
||||
The user is granted admin access if the regex matches the attribute value.
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
auth:
|
||||
oidc:
|
||||
- provider_name: "oidc1"
|
||||
# ... other settings
|
||||
field_map:
|
||||
is_admin: "wg_admin_prop"
|
||||
admin_mapping:
|
||||
admin_value_regex: "^true$"
|
||||
```
|
||||
The example above will grant admin access to users with the `wg_admin_prop` attribute set to `true`.
|
||||
|
||||
**Group membership mapping** can be achieved by setting the `admin_group_regex` and `user_groups` property.
|
||||
The `admin_group_regex` property is a regular expression that is matched against the group names of the user.
|
||||
The user is granted admin access if the regex matches any of the group names.
|
||||
|
||||
Example:
|
||||
```yaml
|
||||
auth:
|
||||
oidc:
|
||||
- provider_name: "oidc1"
|
||||
# ... other settings
|
||||
field_map:
|
||||
user_groups: "groups"
|
||||
admin_mapping:
|
||||
admin_group_regex: "^the-admin-group$"
|
||||
```
|
||||
The example above will grant admin access to users who are members of the `the-admin-group` group.
|
||||
|
||||
|
||||
### LDAP Authentication
|
||||
|
||||
WireGuard Portal supports LDAP authentication. You can use any LDAP server that supports the LDAP protocol, such as Active Directory or OpenLDAP.
|
||||
Multiple LDAP servers can be configured in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||
WireGuard Portal remembers the authentication provider of the user and therefore avoids conflicts between multiple LDAP providers.
|
||||
|
||||
To configure LDAP authentication, create a new [`ldap`](../configuration/overview.md#ldap) authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||
|
||||
#### Limiting Login to Specific Users
|
||||
|
||||
You can limit the login to specific users by setting the `login_filter` property for LDAP provider. This filter uses the LDAP search filter syntax.
|
||||
The username can be inserted into the query by placing the `{{login_identifier}}` placeholder in the filter. This placeholder will then be replaced with the username entered by the user during login.
|
||||
|
||||
For example, if you want to allow only users with the `objectClass` attribute set to `organizationalPerson` to log in, set the property as follows:
|
||||
|
||||
```yaml
|
||||
auth:
|
||||
ldap:
|
||||
- provider_name: "ldap1"
|
||||
# ... other settings
|
||||
login_filter: "(&(objectClass=organizationalPerson)(uid={{login_identifier}}))"
|
||||
```
|
||||
|
||||
The `login_filter` should always be designed to return at most one user.
|
||||
|
||||
#### Limit Login to Existing Users
|
||||
|
||||
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for LDAP providers.
|
||||
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||
|
||||
#### Admin Mapping
|
||||
|
||||
You can map users to admin roles based on their group membership in the LDAP server. To do this, set the `admin_group` and `memberof` property for the provider.
|
||||
The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin.
|
||||
All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access.
|
||||
|
||||
|
||||
## UI and API Access
|
||||
|
||||
WireGuard Portal provides a web UI and a REST API for user interaction. It is important to secure these interfaces to prevent unauthorized access and data breaches.
|
||||
|
||||
### HTTPS
|
||||
It is recommended to use HTTPS for all communication with the portal to prevent eavesdropping.
|
||||
|
||||
Event though, WireGuard Portal supports HTTPS out of the box, it is recommended to use a reverse proxy like Nginx or Traefik to handle SSL termination and other security features.
|
||||
A detailed explanation is available in the [Reverse Proxy](../getting-started/reverse-proxy.md) section.
|
285
docs/documentation/usage/webhooks.md
Normal file
@@ -0,0 +1,285 @@
|
||||
|
||||
Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows.
|
||||
|
||||
When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.
|
||||
The payload contains event-specific data in JSON format.
|
||||
|
||||
## Configuration
|
||||
|
||||
All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook).
|
||||
|
||||
A basic webhook configuration looks like this:
|
||||
|
||||
```yaml
|
||||
webhook:
|
||||
url: https://your-service.example.com/webhook
|
||||
```
|
||||
|
||||
### Security
|
||||
|
||||
Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request.
|
||||
You can set the shared secret in the webhook configuration:
|
||||
|
||||
```yaml
|
||||
webhook:
|
||||
url: https://your-service.example.com/webhook
|
||||
secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||
```
|
||||
|
||||
You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering.
|
||||
|
||||
## Available Events
|
||||
|
||||
WireGuard Portal supports various events that can trigger webhooks. The following events are available:
|
||||
|
||||
- `create`: Triggered when a new entity is created.
|
||||
- `update`: Triggered when an existing entity is updated.
|
||||
- `delete`: Triggered when an entity is deleted.
|
||||
- `connect`: Triggered when a user connects to the VPN.
|
||||
- `disconnect`: Triggered when a user disconnects from the VPN.
|
||||
|
||||
The following entity models are supported for webhook events:
|
||||
|
||||
- `user`: WireGuard Portal users support creation, update, or deletion events.
|
||||
- `peer`: Peers support creation, update, or deletion events. Via the `peer_metric` entity, you can also receive connection status updates.
|
||||
- `peer_metric`: Peer metrics support connection status updates, such as when a peer connects or disconnects.
|
||||
- `interface`: WireGuard interfaces support creation, update, or deletion events.
|
||||
|
||||
## Payload Structure
|
||||
|
||||
All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved.
|
||||
A common shell structure for webhook payloads is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "create", // The event type, e.g. "create", "update", "delete", "connect", "disconnect"
|
||||
"entity": "user", // The entity type, e.g. "user", "peer", "peer_metric", "interface"
|
||||
"identifier": "the-user-identifier", // Unique identifier of the entity, e.g. user ID or peer ID
|
||||
"payload": {
|
||||
// The payload of the event, e.g. a Peer model.
|
||||
// Detailed model descriptions are provided below.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Payload Models
|
||||
|
||||
All payload models are encoded as JSON objects. Fields with empty values might be omitted in the payload.
|
||||
|
||||
#### User Payload (entity: `user`)
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|----------------|-------------|-----------------------------------|
|
||||
| CreatedBy | string | Creator identifier |
|
||||
| UpdatedBy | string | Last updater identifier |
|
||||
| CreatedAt | time.Time | Time of creation |
|
||||
| UpdatedAt | time.Time | Time of last update |
|
||||
| Identifier | string | Unique user identifier |
|
||||
| Email | string | User email |
|
||||
| Source | string | Authentication source |
|
||||
| ProviderName | string | Name of auth provider |
|
||||
| IsAdmin | bool | Whether user has admin privileges |
|
||||
| Firstname | string | User's first name (optional) |
|
||||
| Lastname | string | User's last name (optional) |
|
||||
| Phone | string | Contact phone number (optional) |
|
||||
| Department | string | User's department (optional) |
|
||||
| Notes | string | Additional notes (optional) |
|
||||
| Disabled | *time.Time | When user was disabled |
|
||||
| DisabledReason | string | Reason for deactivation |
|
||||
| Locked | *time.Time | When user account was locked |
|
||||
| LockedReason | string | Reason for being locked |
|
||||
|
||||
|
||||
#### Peer Payload (entity: `peer`)
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|----------------------|------------|----------------------------------------|
|
||||
| CreatedBy | string | Creator identifier |
|
||||
| UpdatedBy | string | Last updater identifier |
|
||||
| CreatedAt | time.Time | Creation timestamp |
|
||||
| UpdatedAt | time.Time | Last update timestamp |
|
||||
| Endpoint | string | Peer endpoint address |
|
||||
| EndpointPublicKey | string | Public key of peer endpoint |
|
||||
| AllowedIPsStr | string | Allowed IPs |
|
||||
| ExtraAllowedIPsStr | string | Extra allowed IPs |
|
||||
| PresharedKey | string | Pre-shared key for encryption |
|
||||
| PersistentKeepalive | int | Keepalive interval in seconds |
|
||||
| DisplayName | string | Display name of the peer |
|
||||
| Identifier | string | Unique identifier |
|
||||
| UserIdentifier | string | Associated user ID (optional) |
|
||||
| InterfaceIdentifier | string | Interface this peer is attached to |
|
||||
| Disabled | *time.Time | When the peer was disabled |
|
||||
| DisabledReason | string | Reason for being disabled |
|
||||
| ExpiresAt | *time.Time | Expiration date |
|
||||
| Notes | string | Notes for this peer |
|
||||
| AutomaticallyCreated | bool | Whether peer was auto-generated |
|
||||
| PrivateKey | string | Peer private key |
|
||||
| PublicKey | string | Peer public key |
|
||||
| InterfaceType | string | Type of the peer interface |
|
||||
| Addresses | []string | IP addresses |
|
||||
| CheckAliveAddress | string | Address used for alive checks |
|
||||
| DnsStr | string | DNS servers |
|
||||
| DnsSearchStr | string | DNS search domains |
|
||||
| Mtu | int | MTU (Maximum Transmission Unit) |
|
||||
| FirewallMark | uint32 | Firewall mark (optional) |
|
||||
| RoutingTable | string | Custom routing table (optional) |
|
||||
| PreUp | string | Command before bringing up interface |
|
||||
| PostUp | string | Command after bringing up interface |
|
||||
| PreDown | string | Command before bringing down interface |
|
||||
| PostDown | string | Command after bringing down interface |
|
||||
|
||||
|
||||
#### Interface Payload (entity: `interface`)
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|----------------------------|------------|----------------------------------------|
|
||||
| CreatedBy | string | Creator identifier |
|
||||
| UpdatedBy | string | Last updater identifier |
|
||||
| CreatedAt | time.Time | Creation timestamp |
|
||||
| UpdatedAt | time.Time | Last update timestamp |
|
||||
| Identifier | string | Unique identifier |
|
||||
| PrivateKey | string | Private key for the interface |
|
||||
| PublicKey | string | Public key for the interface |
|
||||
| ListenPort | int | Listening port |
|
||||
| Addresses | []string | IP addresses |
|
||||
| DnsStr | string | DNS servers |
|
||||
| DnsSearchStr | string | DNS search domains |
|
||||
| Mtu | int | MTU (Maximum Transmission Unit) |
|
||||
| FirewallMark | uint32 | Firewall mark |
|
||||
| RoutingTable | string | Custom routing table |
|
||||
| PreUp | string | Command before bringing up interface |
|
||||
| PostUp | string | Command after bringing up interface |
|
||||
| PreDown | string | Command before bringing down interface |
|
||||
| PostDown | string | Command after bringing down interface |
|
||||
| SaveConfig | bool | Whether to save config to file |
|
||||
| DisplayName | string | Human-readable name |
|
||||
| Type | string | Type of interface |
|
||||
| DriverType | string | Driver used |
|
||||
| Disabled | *time.Time | When the interface was disabled |
|
||||
| DisabledReason | string | Reason for being disabled |
|
||||
| PeerDefNetworkStr | string | Default peer network configuration |
|
||||
| PeerDefDnsStr | string | Default peer DNS servers |
|
||||
| PeerDefDnsSearchStr | string | Default peer DNS search domains |
|
||||
| PeerDefEndpoint | string | Default peer endpoint |
|
||||
| PeerDefAllowedIPsStr | string | Default peer allowed IPs |
|
||||
| PeerDefMtu | int | Default peer MTU |
|
||||
| PeerDefPersistentKeepalive | int | Default keepalive value |
|
||||
| PeerDefFirewallMark | uint32 | Default firewall mark for peers |
|
||||
| PeerDefRoutingTable | string | Default routing table for peers |
|
||||
| PeerDefPreUp | string | Default peer pre-up command |
|
||||
| PeerDefPostUp | string | Default peer post-up command |
|
||||
| PeerDefPreDown | string | Default peer pre-down command |
|
||||
| PeerDefPostDown | string | Default peer post-down command |
|
||||
|
||||
|
||||
#### Peer Metrics Payload (entity: `peer_metric`)
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|------------|------------|----------------------------|
|
||||
| Status | PeerStatus | Current status of the peer |
|
||||
| Peer | Peer | Peer data |
|
||||
|
||||
`PeerStatus` sub-structure:
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|------------------|------------|------------------------------|
|
||||
| UpdatedAt | time.Time | Time of last status update |
|
||||
| IsConnected | bool | Is peer currently connected |
|
||||
| IsPingable | bool | Can peer be pinged |
|
||||
| LastPing | *time.Time | Time of last successful ping |
|
||||
| BytesReceived | uint64 | Bytes received from peer |
|
||||
| BytesTransmitted | uint64 | Bytes sent to peer |
|
||||
| Endpoint | string | Last known endpoint |
|
||||
| LastHandshake | *time.Time | Last successful handshake |
|
||||
| LastSessionStart | *time.Time | Time the last session began |
|
||||
|
||||
|
||||
### Example Payloads
|
||||
|
||||
The following payload is an example of a webhook event when a peer connects to the VPN:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "connect",
|
||||
"entity": "peer_metric",
|
||||
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"payload": {
|
||||
"Status": {
|
||||
"UpdatedAt": "2025-06-27T22:20:08.734900034+02:00",
|
||||
"IsConnected": true,
|
||||
"IsPingable": false,
|
||||
"BytesReceived": 212,
|
||||
"BytesTransmitted": 2884,
|
||||
"Endpoint": "10.55.66.77:58756",
|
||||
"LastHandshake": "2025-06-27T22:19:46.580842776+02:00",
|
||||
"LastSessionStart": "2025-06-27T22:19:46.580842776+02:00"
|
||||
},
|
||||
"Peer": {
|
||||
"CreatedBy": "admin@wgportal.local",
|
||||
"UpdatedBy": "admin@wgportal.local",
|
||||
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
|
||||
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
|
||||
"Endpoint": "10.55.66.1:51820",
|
||||
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
|
||||
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
|
||||
"ExtraAllowedIPsStr": "",
|
||||
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
|
||||
"PersistentKeepalive": 16,
|
||||
"DisplayName": "Peer Fb5TaziA",
|
||||
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"UserIdentifier": "admin@wgportal.local",
|
||||
"InterfaceIdentifier": "wgTesting",
|
||||
"AutomaticallyCreated": false,
|
||||
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
|
||||
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"InterfaceType": "client",
|
||||
"Addresses": [
|
||||
"10.11.12.10/32",
|
||||
"fdfd:d3ad:c0de:1234::a/128"
|
||||
],
|
||||
"CheckAliveAddress": "",
|
||||
"DnsStr": "",
|
||||
"DnsSearchStr": "",
|
||||
"Mtu": 1420
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here is another example of a webhook event when a peer is updated:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "update",
|
||||
"entity": "peer",
|
||||
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"payload": {
|
||||
"CreatedBy": "admin@wgportal.local",
|
||||
"UpdatedBy": "admin@wgportal.local",
|
||||
"CreatedAt": "2025-06-26T21:43:49.251839574+02:00",
|
||||
"UpdatedAt": "2025-06-27T22:18:39.67763985+02:00",
|
||||
"Endpoint": "10.55.66.1:51820",
|
||||
"EndpointPublicKey": "eiVibpi3C2PUPcx2kwA5s09OgHx7AEaKMd33k0LQ5mM=",
|
||||
"AllowedIPsStr": "10.11.12.0/24,fdfd:d3ad:c0de:1234::/64",
|
||||
"ExtraAllowedIPsStr": "",
|
||||
"PresharedKey": "p9DDeLUSLOdQcjS8ZsBAiqUzwDIUvTyzavRZFuzhvyE=",
|
||||
"PersistentKeepalive": 16,
|
||||
"DisplayName": "Peer Fb5TaziA",
|
||||
"Identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"UserIdentifier": "admin@wgportal.local",
|
||||
"InterfaceIdentifier": "wgTesting",
|
||||
"AutomaticallyCreated": false,
|
||||
"PrivateKey": "QBFNBe+7J49ergH0ze2TGUJMFrL/2bOL50Z2cgluYW8=",
|
||||
"PublicKey": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||
"InterfaceType": "client",
|
||||
"Addresses": [
|
||||
"10.11.12.10/32",
|
||||
"fdfd:d3ad:c0de:1234::a/128"
|
||||
],
|
||||
"CheckAliveAddress": "",
|
||||
"DnsStr": "",
|
||||
"DnsSearchStr": "",
|
||||
"Mtu": 1420
|
||||
}
|
||||
}
|
||||
```
|
4
docs/index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
template: layouts/home.html
|
||||
title: WireGuard Portal
|
||||
---
|
2
docs/javascript/img-comparison-slider.js
Normal file
1
docs/javascript/img-comparison-slider.js.map
Normal file
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;
|
||||
}
|
15
docs/stylesheets/img-comparison-slider.css
Normal file
@@ -0,0 +1,15 @@
|
||||
img-comparison-slider {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
img-comparison-slider [slot='second'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img-comparison-slider.rendered {
|
||||
visibility: inherit;
|
||||
}
|
||||
|
||||
img-comparison-slider.rendered [slot='second'] {
|
||||
display: unset;
|
||||
}
|
509
docs/theme-overrides/layouts/home.html
Normal file
@@ -0,0 +1,509 @@
|
||||
|
||||
{% 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);
|
||||
}
|
||||
|
||||
.before,
|
||||
.after {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.after figcaption {
|
||||
background: #fff;
|
||||
font-weight: bold;
|
||||
border: 1px solid #c0c0c0;
|
||||
color: #000000;
|
||||
opacity: 0.9;
|
||||
padding: 9px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
transform: translateY(-100%);
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.before figcaption {
|
||||
background: #000;
|
||||
font-weight: bold;
|
||||
border: 1px solid #c0c0c0;
|
||||
color: #ffffff;
|
||||
opacity: 0.9;
|
||||
padding: 9px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
transform: translateY(-100%);
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.before figcaption {
|
||||
left: 0px;
|
||||
}
|
||||
.after figcaption {
|
||||
right: 0px;
|
||||
}
|
||||
.custom-animated-handle {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.slider-with-animated-handle:hover .custom-animated-handle {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.md-typeset img-comparison-slider figure {
|
||||
margin: initial;
|
||||
}
|
||||
|
||||
.first-overlay {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
</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">
|
||||
<div>
|
||||
<img-comparison-slider hover="hover">
|
||||
<figure slot="first" class="before">
|
||||
<img src="{{config.site_url}}/assets/images/wgportal_light.png" alt="Light Mode"/>
|
||||
<figcaption>Light Mode</figcaption>
|
||||
</figure>
|
||||
<figure slot="second" class="after">
|
||||
<img src="{{config.site_url}}/assets/images/wgportal_dark.png" alt="Dark Mode"/>
|
||||
<figcaption>Dark Mode</figcaption>
|
||||
</figure>
|
||||
<svg slot="handle" class="custom-animated-handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
|
||||
<!-- Left arrow (dark) -->
|
||||
<path d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2"
|
||||
stroke="#1a1a1a"
|
||||
fill="#1a1a1a"
|
||||
stroke-width="1"
|
||||
vector-effect="non-scaling-stroke">
|
||||
</path>
|
||||
<!-- Right arrow (white) -->
|
||||
<path d="M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2"
|
||||
stroke="#fff"
|
||||
fill="#fff"
|
||||
stroke-width="1"
|
||||
vector-effect="non-scaling-stroke">
|
||||
</path>
|
||||
</svg>
|
||||
</img-comparison-slider>
|
||||
</div>
|
||||
</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>
|
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
@@ -24,7 +24,7 @@
|
||||
<div id="toasts"></div>
|
||||
|
||||
<!-- main application -->
|
||||
<div id="app"></div>
|
||||
<div id="app" class="d-flex flex-column flex-grow-1"></div>
|
||||
|
||||
<!-- vue teleport will add modals and dialogs here -->
|
||||
<div id="modals"></div>
|
||||
|
2813
frontend/package-lock.json
generated
@@ -8,24 +8,28 @@
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@kyvg/vue3-notification": "^2.9.1",
|
||||
"@fontsource/nunito-sans": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootswatch": "^5.3.0",
|
||||
"flag-icons": "^6.7.0",
|
||||
"is-cidr": "^5.0.3",
|
||||
"is-ip": "^5.0.0",
|
||||
"pinia": "^2.1.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||
"bootstrap": "^5.3.7",
|
||||
"bootswatch": "^5.3.7",
|
||||
"flag-icons": "^7.3.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"is-cidr": "^5.1.1",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.9"
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vite": "6.3.4"
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import {computed, getCurrentInstance, onMounted, ref} from "vue";
|
||||
import {authStore} from "./stores/auth";
|
||||
import {securityStore} from "./stores/security";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
||||
import { authStore } from "./stores/auth";
|
||||
import { securityStore } from "./stores/security";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { Notifications } from "@kyvg/vue3-notification";
|
||||
|
||||
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
||||
const auth = authStore()
|
||||
@@ -13,6 +14,10 @@ const settings = settingsStore()
|
||||
onMounted(async () => {
|
||||
console.log("Starting WireGuard Portal frontend...");
|
||||
|
||||
// restore theme from localStorage
|
||||
const theme = localStorage.getItem('wgTheme') || 'light';
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
|
||||
await sec.LoadSecurityProperties();
|
||||
await auth.LoadProviders();
|
||||
|
||||
@@ -39,19 +44,54 @@ const switchLanguage = function (lang) {
|
||||
}
|
||||
}
|
||||
|
||||
const switchTheme = function (theme) {
|
||||
if (document.documentElement.getAttribute('data-bs-theme') !== theme) {
|
||||
localStorage.setItem('wgTheme', theme);
|
||||
document.documentElement.setAttribute('data-bs-theme', theme);
|
||||
}
|
||||
}
|
||||
|
||||
const languageFlag = computed(() => {
|
||||
// `this` points to the component instance
|
||||
let lang = appGlobal.$i18n.locale.toLowerCase();
|
||||
if (lang === "en") {
|
||||
lang = "us";
|
||||
if (!appGlobal.$i18n.availableLocales.includes(lang)) {
|
||||
lang = appGlobal.$i18n.fallbackLocale;
|
||||
}
|
||||
return "fi-" + lang;
|
||||
const langMap = {
|
||||
en: "us",
|
||||
pt: "pt",
|
||||
uk: "ua",
|
||||
zh: "cn",
|
||||
ko: "kr",
|
||||
|
||||
};
|
||||
return "fi-" + (langMap[lang] || lang);
|
||||
})
|
||||
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
let displayName = "Unknown";
|
||||
if (auth.IsAuthenticated) {
|
||||
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
|
||||
displayName = auth.User.Identifier;
|
||||
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
|
||||
displayName = auth.User.Lastname;
|
||||
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
|
||||
displayName = auth.User.Firstname;
|
||||
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
|
||||
displayName = auth.User.Firstname + " " + auth.User.Lastname;
|
||||
}
|
||||
}
|
||||
|
||||
// pad string to 20 characters so that the menu is always the same size on desktop
|
||||
if (displayName.length < 20 && window.innerWidth > 992) {
|
||||
displayName = displayName.padStart(20, "\u00A0");
|
||||
}
|
||||
return displayName;
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,7 +99,7 @@ const currentYear = ref(new Date().getFullYear())
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img alt="WireGuard Portal" src="/img/header-logo.png" /></a>
|
||||
<a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
|
||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@@ -76,24 +116,43 @@ const currentYear = ref(new Date().getFullYear())
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"
|
||||
role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
||||
href="#" role="button">{{ userDisplayName }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly') || settings.Setting('WebAuthnEnabled')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout">
|
||||
<i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!auth.IsAuthenticated" class="nav-item">
|
||||
<RouterLink :to="{ name: 'login' }" class="nav-link">
|
||||
<i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'login' }" class="nav-link"><i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}</RouterLink>
|
||||
</div>
|
||||
<div class="nav-item dropdown" data-bs-theme="light">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="theme-menu" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme">
|
||||
<i class="fa-solid fa-circle-half-stroke"></i>
|
||||
<span class="d-lg-none ms-2">Toggle theme</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('light')" aria-pressed="false">
|
||||
<i class="fa-solid fa-sun"></i><span class="ms-2">Light</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" @click.prevent="switchTheme('dark')" aria-pressed="true">
|
||||
<i class="fa-solid fa-moon"></i><span class="ms-2">Dark</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,10 +170,19 @@ const currentYear = ref(new Date().getFullYear())
|
||||
<div class="col-6 text-end">
|
||||
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
|
||||
<div class="btn-group" role="group">
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0" data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn flag-button pe-0"
|
||||
data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ko')"><span class="fi fi-kr"></span> 한국어</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('pt')"><span class="fi fi-pt"></span> Português</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,4 +193,30 @@ const currentYear = ref(new Date().getFullYear())
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
.flag-button:active,.flag-button:hover,.flag-button:focus,.flag-button:checked,.flag-button:disabled,.flag-button:not(:disabled) {
|
||||
border: 1px solid transparent!important;
|
||||
}
|
||||
[data-bs-theme=dark] .form-select {
|
||||
color: #0c0c0c!important;
|
||||
background-color: #c1c1c1!important;
|
||||
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")!important;
|
||||
}
|
||||
[data-bs-theme=dark] .form-control {
|
||||
color: #0c0c0c!important;
|
||||
background-color: #c1c1c1!important;
|
||||
}
|
||||
[data-bs-theme=dark] .form-control:focus {
|
||||
color: #0c0c0c!important;
|
||||
background-color: #c1c1c1!important;
|
||||
}
|
||||
[data-bs-theme=dark] .badge.bg-light {
|
||||
--bs-bg-opacity: 1;
|
||||
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
|
||||
color: var(--bs-badge-color)!important;
|
||||
}
|
||||
[data-bs-theme=dark] span.input-group-text {
|
||||
--bs-bg-opacity: 1;
|
||||
background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
|
||||
color: var(--bs-badge-color)!important;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,107 @@
|
||||
a.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: #888888;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
overflow-break: anywhere;
|
||||
}
|
||||
|
||||
.asc::after {
|
||||
content: " ↑";
|
||||
}
|
||||
|
||||
.desc::after {
|
||||
content: " ↓";
|
||||
}
|
||||
|
||||
/* style the background and the text color of the input ... */
|
||||
.vue-tags-input {
|
||||
max-width: 100% !important;
|
||||
background-color: #f7f7f9 !important;
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
.vue-tags-input .ti-input {
|
||||
padding: 0 0;
|
||||
border: none !important;
|
||||
transition: border-bottom 200ms ease;
|
||||
}
|
||||
|
||||
.vue-tags-input .ti-new-tag-input {
|
||||
background: transparent;
|
||||
color: var(--bs-body-color);
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
}
|
||||
|
||||
|
||||
/* style the placeholders color across all browser */
|
||||
.vue-tags-input ::-webkit-input-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
.vue-tags-input .ti-input::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.vue-tags-input ::-moz-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.vue-tags-input :-ms-input-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.vue-tags-input :-moz-placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
/* default styles for all the tags */
|
||||
.vue-tags-input .ti-tag {
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border: 2px solid var(--bs-body-color);
|
||||
margin: 6px;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] .vue-tags-input .ti-tag {
|
||||
position: relative;
|
||||
background: #3c3c3c;
|
||||
border: 2px solid var(--bs-body-color);
|
||||
margin: 6px;
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
/* the styles if a tag is invalid */
|
||||
.vue-tags-input .ti-tag.ti-invalid {
|
||||
background-color: #e88a74;
|
||||
}
|
||||
|
||||
/* if the user input is invalid, the input color should be red */
|
||||
.vue-tags-input .ti-new-tag-input.ti-invalid {
|
||||
color: #e88a74;
|
||||
}
|
||||
|
||||
/* if a tag or the user input is a duplicate, it should be crossed out */
|
||||
.vue-tags-input .ti-duplicate span,
|
||||
.vue-tags-input .ti-new-tag-input.ti-duplicate {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* if the user presses backspace, the complete tag should be crossed out, to mark it for deletion */
|
||||
.vue-tags-input .ti-tag:after {
|
||||
transition: transform .2s;
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 2px;
|
||||
width: 108%;
|
||||
left: -4%;
|
||||
top: calc(50% - 1px);
|
||||
background-color: #000;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.vue-tags-input .ti-deletion-mark:after {
|
||||
transform: scaleX(1);
|
||||
}
|
20
frontend/src/assets/custom.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
// disable external web fonts
|
||||
$web-font-path: false;
|
||||
|
||||
@import "bootswatch/dist/lux/variables";
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
@import "bootswatch/dist/lux/bootswatch";
|
||||
|
||||
// fix strange border width bug in bootswatch 5.3
|
||||
:root {
|
||||
--bs-border-width: 1px;
|
||||
}
|
||||
|
||||
// for future use, once bootswatch supports @use
|
||||
/*
|
||||
@use "bootswatch/dist/lux/_variables.scss" as lux-vars with (
|
||||
$web-font-path: false
|
||||
);
|
||||
@use "bootstrap/scss/bootstrap" as bs;
|
||||
@use "bootswatch/dist/lux/_bootswatch.scss" as lux-theme;
|
||||
*/
|
@@ -4,17 +4,19 @@ import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from 'vue3-tags-input';
|
||||
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
const settings = settingsStore()
|
||||
|
||||
const props = defineProps({
|
||||
interfaceId: String,
|
||||
@@ -38,7 +40,36 @@ const title = computed(() => {
|
||||
return t("modals.interface-edit.headline-new")
|
||||
})
|
||||
|
||||
const currentTags = ref({
|
||||
Addresses: "",
|
||||
Dns: "",
|
||||
DnsSearch: "",
|
||||
PeerDefNetwork: "",
|
||||
PeerDefAllowedIPs: "",
|
||||
PeerDefDns: "",
|
||||
PeerDefDnsSearch: ""
|
||||
})
|
||||
const formData = ref(freshInterface())
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const isApplyingDefaults = ref(false)
|
||||
|
||||
const isBackendValid = computed(() => {
|
||||
if (!props.visible || !selectedInterface.value) {
|
||||
return true // if modal is not visible or no interface is selected, we don't care about backend validity
|
||||
}
|
||||
|
||||
let backendId = selectedInterface.value.Backend
|
||||
|
||||
let valid = false
|
||||
let availableBackends = settings.Setting('AvailableBackends') || []
|
||||
availableBackends.forEach(backend => {
|
||||
if (backend.Id === backendId) {
|
||||
valid = true
|
||||
}
|
||||
})
|
||||
return valid
|
||||
})
|
||||
|
||||
// functions
|
||||
|
||||
@@ -52,6 +83,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||
formData.value.Mode = interfaces.Prepared.Mode
|
||||
formData.value.Backend = interfaces.Prepared.Backend
|
||||
|
||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
|
||||
@@ -90,6 +122,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = selectedInterface.value.Identifier
|
||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||
formData.value.Mode = selectedInterface.value.Mode
|
||||
formData.value.Backend = selectedInterface.value.Backend
|
||||
|
||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||
formData.value.PrivateKey = selectedInterface.value.PrivateKey
|
||||
@@ -137,97 +170,99 @@ function close() {
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if(isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
formData.value.Addresses = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
if(!isIP(tag.text)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns = tags
|
||||
formData.value.Dns = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch = tags
|
||||
formData.value.DnsSearch = tags.map(tag => tag.text)
|
||||
}
|
||||
|
||||
function handleChangePeerDefNetwork(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if(isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefNetwork = tags
|
||||
formData.value.PeerDefNetwork = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if(isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefAllowedIPs = tags
|
||||
formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
if(!isIP(tag.text)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefDns = tags
|
||||
formData.value.PeerDefDns = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDnsSearch(tags) {
|
||||
formData.value.PeerDefDnsSearch = tags
|
||||
formData.value.PeerDefDnsSearch = tags.map(tag => tag.text)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (props.interfaceId!=='#NEW#') {
|
||||
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
|
||||
@@ -242,6 +277,8 @@ async function save() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +287,8 @@ async function applyPeerDefaults() {
|
||||
return; // do nothing for new interfaces
|
||||
}
|
||||
|
||||
if (isApplyingDefaults.value) return
|
||||
isApplyingDefaults.value = true
|
||||
try {
|
||||
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
||||
|
||||
@@ -267,10 +306,14 @@ async function applyPeerDefaults() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isApplyingDefaults.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (isDeleting.value) return
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||
close()
|
||||
@@ -281,6 +324,8 @@ async function del() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,13 +350,22 @@ async function del() {
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
|
||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
||||
<select v-model="formData.Mode" class="form-select">
|
||||
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
||||
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
||||
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
||||
<select v-model="formData.Mode" class="form-select">
|
||||
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
||||
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
||||
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4" for="ifaceBackendSelector">{{ $t('modals.interface-edit.backend.label') }}</label>
|
||||
<select id="ifaceBackendSelector" v-model="formData.Backend" class="form-select" aria-describedby="backendHelp">
|
||||
<option v-for="backend in settings.Setting('AvailableBackends')" :value="backend.Id">{{ backend.Id === 'local' ? $t(backend.Name) : backend.Name }}</option>
|
||||
</select>
|
||||
<small v-if="!isBackendValid" id="backendHelp" class="form-text text-warning">{{ $t('modals.interface-edit.backend.invalid-label') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
|
||||
@@ -322,22 +376,26 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="text">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeAddresses"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode==='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
||||
@@ -345,31 +403,41 @@ async function del() {
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||
:tags="formData.Dns.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:validation="validateIP()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeDns"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||
:tags="formData.DnsSearch.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:validation="validateDomain()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
|
||||
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<div class="form-group col-md-6" v-if="formData.Backend==='local'">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6" v-else>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row" v-if="formData.Backend==='local'">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
|
||||
@@ -420,36 +488,52 @@ async function del() {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
|
||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefNetwork"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.PeerDefNetwork"
|
||||
:tags="formData.PeerDefNetwork.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangePeerDefNetwork"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
|
||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.PeerDefAllowedIPs"
|
||||
:tags="formData.PeerDefAllowedIPs.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangePeerDefDns"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDns"
|
||||
:tags="formData.PeerDefDns.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:validation="validateIP()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangePeerDefDns"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangePeerDefDnsSearch"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDnsSearch"
|
||||
:tags="formData.PeerDefDnsSearch.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:validation="validateDomain()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangePeerDefDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
@@ -493,16 +577,25 @@ async function del() {
|
||||
</fieldset>
|
||||
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">{{ $t('modals.interface-edit.button-apply-defaults') }}</button>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults" :disabled="isApplyingDefaults">
|
||||
<span v-if="isApplyingDefaults" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('modals.interface-edit.button-apply-defaults') }}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.save') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@@ -1,20 +1,22 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { peerStore } from "@/stores/peers";
|
||||
import { interfaceStore } from "@/stores/interfaces";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { isIP } from 'is-ip';
|
||||
import { freshPeer, freshInterface } from '@/helpers/models';
|
||||
import { profileStore } from "@/stores/profile";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
const profile = profileStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
@@ -24,7 +26,16 @@ const props = defineProps({
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
return peers.Find(props.peerId)
|
||||
let p = peers.Find(props.peerId)
|
||||
|
||||
if (!p) {
|
||||
if (!!props.peerId || props.peerId.length) {
|
||||
p = profile.peers.find((p) => p.Identifier === props.peerId)
|
||||
} else {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
@@ -54,126 +65,133 @@ const title = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const currentTags = ref({
|
||||
Addresses: "",
|
||||
AllowedIPs: "",
|
||||
ExtraAllowedIPs: "",
|
||||
Dns: "",
|
||||
DnsSearch: ""
|
||||
})
|
||||
const formData = ref(freshPeer())
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
console.log(selectedPeer.value)
|
||||
if (!selectedPeer.value) {
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
if (!selectedPeer.value) {
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||
formData.value.Notes = peers.Prepared.Notes
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||
formData.value.Notes = peers.Prepared.Notes
|
||||
|
||||
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||
|
||||
formData.value.Mode = peers.Prepared.Mode
|
||||
formData.value.Mode = peers.Prepared.Mode
|
||||
|
||||
formData.value.Addresses = peers.Prepared.Addresses
|
||||
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||
formData.value.Dns = peers.Prepared.Dns
|
||||
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||
formData.value.Mtu = peers.Prepared.Mtu
|
||||
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||
formData.value.Addresses = peers.Prepared.Addresses
|
||||
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||
formData.value.Dns = peers.Prepared.Dns
|
||||
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||
formData.value.Mtu = peers.Prepared.Mtu
|
||||
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = peers.Prepared.PreUp
|
||||
formData.value.PostUp = peers.Prepared.PostUp
|
||||
formData.value.PreDown = peers.Prepared.PreDown
|
||||
formData.value.PostDown = peers.Prepared.PostDown
|
||||
formData.value.PreUp = peers.Prepared.PreUp
|
||||
formData.value.PostUp = peers.Prepared.PostUp
|
||||
formData.value.PreDown = peers.Prepared.PreDown
|
||||
formData.value.PostDown = peers.Prepared.PostDown
|
||||
|
||||
} else { // fill existing data
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||
formData.value.Notes = selectedPeer.value.Notes
|
||||
} else { // fill existing data
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||
formData.value.Notes = selectedPeer.value.Notes
|
||||
|
||||
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||
|
||||
formData.value.Mode = selectedPeer.value.Mode
|
||||
formData.value.Mode = selectedPeer.value.Mode
|
||||
|
||||
formData.value.Addresses = selectedPeer.value.Addresses
|
||||
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||
formData.value.Dns = selectedPeer.value.Dns
|
||||
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||
formData.value.Mtu = selectedPeer.value.Mtu
|
||||
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||
formData.value.Addresses = selectedPeer.value.Addresses
|
||||
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||
formData.value.Dns = selectedPeer.value.Dns
|
||||
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||
formData.value.Mtu = selectedPeer.value.Mtu
|
||||
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedPeer.value.PreUp
|
||||
formData.value.PostUp = selectedPeer.value.PostUp
|
||||
formData.value.PreDown = selectedPeer.value.PreDown
|
||||
formData.value.PostDown = selectedPeer.value.PostDown
|
||||
formData.value.PreUp = selectedPeer.value.PreUp
|
||||
formData.value.PostUp = selectedPeer.value.PostUp
|
||||
formData.value.PreDown = selectedPeer.value.PreDown
|
||||
formData.value.PostDown = selectedPeer.value.PostDown
|
||||
|
||||
if (!formData.value.Endpoint.Overridable ||
|
||||
!formData.value.EndpointPublicKey.Overridable ||
|
||||
!formData.value.AllowedIPs.Overridable ||
|
||||
!formData.value.PersistentKeepalive.Overridable ||
|
||||
!formData.value.Dns.Overridable ||
|
||||
!formData.value.DnsSearch.Overridable ||
|
||||
!formData.value.Mtu.Overridable ||
|
||||
!formData.value.FirewallMark.Overridable ||
|
||||
!formData.value.RoutingTable.Overridable ||
|
||||
!formData.value.PreUp.Overridable ||
|
||||
!formData.value.PostUp.Overridable ||
|
||||
!formData.value.PreDown.Overridable ||
|
||||
!formData.value.PostDown.Overridable) {
|
||||
formData.value.IgnoreGlobalSettings = true
|
||||
}
|
||||
}
|
||||
if (!formData.value.Endpoint.Overridable ||
|
||||
!formData.value.EndpointPublicKey.Overridable ||
|
||||
!formData.value.AllowedIPs.Overridable ||
|
||||
!formData.value.PersistentKeepalive.Overridable ||
|
||||
!formData.value.Dns.Overridable ||
|
||||
!formData.value.DnsSearch.Overridable ||
|
||||
!formData.value.Mtu.Overridable ||
|
||||
!formData.value.FirewallMark.Overridable ||
|
||||
!formData.value.RoutingTable.Overridable ||
|
||||
!formData.value.PreUp.Overridable ||
|
||||
!formData.value.PostUp.Overridable ||
|
||||
!formData.value.PreDown.Overridable ||
|
||||
!formData.value.PostDown.Overridable) {
|
||||
formData.value.IgnoreGlobalSettings = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => {
|
||||
formData.value.Endpoint.Overridable = !newValue
|
||||
formData.value.EndpointPublicKey.Overridable = !newValue
|
||||
formData.value.AllowedIPs.Overridable = !newValue
|
||||
formData.value.PersistentKeepalive.Overridable = !newValue
|
||||
formData.value.Dns.Overridable = !newValue
|
||||
formData.value.DnsSearch.Overridable = !newValue
|
||||
formData.value.Mtu.Overridable = !newValue
|
||||
formData.value.FirewallMark.Overridable = !newValue
|
||||
formData.value.RoutingTable.Overridable = !newValue
|
||||
formData.value.PreUp.Overridable = !newValue
|
||||
formData.value.PostUp.Overridable = !newValue
|
||||
formData.value.PreDown.Overridable = !newValue
|
||||
formData.value.PostDown.Overridable = !newValue
|
||||
}
|
||||
formData.value.Endpoint.Overridable = !newValue
|
||||
formData.value.EndpointPublicKey.Overridable = !newValue
|
||||
formData.value.AllowedIPs.Overridable = !newValue
|
||||
formData.value.PersistentKeepalive.Overridable = !newValue
|
||||
formData.value.Dns.Overridable = !newValue
|
||||
formData.value.DnsSearch.Overridable = !newValue
|
||||
formData.value.Mtu.Overridable = !newValue
|
||||
formData.value.FirewallMark.Overridable = !newValue
|
||||
formData.value.RoutingTable.Overridable = !newValue
|
||||
formData.value.PreUp.Overridable = !newValue
|
||||
formData.value.PostUp.Overridable = !newValue
|
||||
formData.value.PreDown.Overridable = !newValue
|
||||
formData.value.PostDown.Overridable = !newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
|
||||
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||
formData.value.ExpiresAt = "" // reset expiry date
|
||||
}
|
||||
}
|
||||
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||
formData.value.ExpiresAt = "" // reset expiry date
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
@@ -184,104 +202,110 @@ function close() {
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if (isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
if (validInput) {
|
||||
formData.value.Addresses = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if (isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.AllowedIPs.Value = tags
|
||||
if (validInput) {
|
||||
formData.value.AllowedIPs.Value = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeExtraAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
if (isCidr(tag.text) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.ExtraAllowedIPs = tags
|
||||
if (validInput) {
|
||||
formData.value.ExtraAllowedIPs = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
if (!isIP(tag.text)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
text: tag.text + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns.Value = tags
|
||||
if (validInput) {
|
||||
formData.value.Dns.Value = tags.map(tag => tag.text)
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch.Value = tags
|
||||
formData.value.DnsSearch.Value = tags.map(tag => tag.text)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (props.peerId!=='#NEW#') {
|
||||
if (props.peerId !== '#NEW#') {
|
||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||
} else {
|
||||
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to save peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (isDeleting.value) return
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to delete peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,87 +318,117 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')" v-model="formData.DisplayName">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
|
||||
v-model="formData.DisplayName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.linked-user.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')" v-model="formData.UserIdentifier">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')"
|
||||
v-model="formData.UserIdentifier">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='server'">
|
||||
<div class="form-group" v-if="selectedInterface.Mode === 'server'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required v-model="formData.PrivateKey">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
v-model="formData.PrivateKey">
|
||||
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required v-model="formData.PublicKey">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')" v-model="formData.PresharedKey">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
v-model="formData.PresharedKey">
|
||||
</div>
|
||||
<div class="form-group" v-if="formData.Mode==='client'">
|
||||
<div class="form-group" v-if="formData.Mode === 'client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint-public-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')" v-model="formData.EndpointPublicKey.Value">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')"
|
||||
v-model="formData.EndpointPublicKey.Value">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='client'">
|
||||
<div class="form-group" v-if="selectedInterface.Mode === 'client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')" v-model="formData.Endpoint.Value">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')"
|
||||
v-model="formData.Endpoint.Value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeAddresses" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
|
||||
<vue-tags-input class="form-control" v-model="currentTags.AllowedIPs"
|
||||
:tags="formData.AllowedIPs.Value.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAllowedIPs"/>
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeAllowedIPs" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
|
||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeExtraAllowedIPs"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.ExtraAllowedIPs"
|
||||
:tags="formData.ExtraAllowedIPs.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||
:validation="validateCIDR()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeExtraAllowedIPs" />
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||
:tags="formData.Dns.Value.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||
:validation="validateIP()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeDns" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||
:tags="formData.DnsSearch.Value.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||
:validation="validateDomain()"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeDnsSearch" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')" v-model="formData.PersistentKeepalive.Value">
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
|
||||
v-model="formData.PersistentKeepalive.Value">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')" v-model="formData.Mtu.Value">
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
|
||||
v-model="formData.Mtu.Value">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -382,19 +436,23 @@ async function del() {
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@@ -403,7 +461,7 @@ async function del() {
|
||||
<div class="form-group col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||
<label class="form-check-label" >{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||
<label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings">
|
||||
@@ -412,20 +470,26 @@ async function del() {
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
|
||||
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01" v-model="formData.ExpiresAt">
|
||||
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
|
||||
v-model="formData.ExpiresAt">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.peerId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.save') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
@@ -5,7 +5,7 @@ import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -32,11 +32,13 @@ const selectedInterface = computed(() => {
|
||||
function freshForm() {
|
||||
return {
|
||||
Identifiers: [],
|
||||
Suffix: "",
|
||||
Prefix: "",
|
||||
}
|
||||
}
|
||||
|
||||
const currentTag = ref("")
|
||||
const formData = ref(freshForm())
|
||||
const isSaving = ref(false)
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
@@ -55,16 +57,19 @@ function close() {
|
||||
}
|
||||
|
||||
function handleChangeUserIdentifiers(tags) {
|
||||
formData.value.Identifiers = tags
|
||||
formData.value.Identifiers = tags.map(tag => tag.text)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
if (formData.value.Identifiers.length === 0) {
|
||||
notify({
|
||||
title: "Missing Identifiers",
|
||||
text: "At least one identifier is required to create a new peer.",
|
||||
type: 'error',
|
||||
})
|
||||
isSaving.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,6 +83,8 @@ async function save() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,21 +96,28 @@ async function save() {
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Identifiers"
|
||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
@on-tags-changed="handleChangeUserIdentifiers"/>
|
||||
<vue-tags-input class="form-control" v-model="currentTag"
|
||||
:tags="formData.Identifiers.map(str => ({ text: str }))"
|
||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||
:add-on-key="[13, 188, 32, 9]"
|
||||
:save-on-key="[13, 188, 32, 9]"
|
||||
:allow-edit-tags="true"
|
||||
:separators="[',', ';', ' ']"
|
||||
@tags-changed="handleChangeUserIdentifiers"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Prefix">
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.save') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@@ -1,19 +1,23 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {freshInterface, freshPeer, freshStats} from '@/helpers/models';
|
||||
import { peerStore } from "@/stores/peers";
|
||||
import { interfaceStore } from "@/stores/interfaces";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { freshInterface, freshPeer, freshStats } from '@/helpers/models';
|
||||
import Prism from "vue-prism-component";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
import { apiWrapper } from "@/helpers/fetch-wrapper";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const settings = settingsStore()
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
const profile = profileStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
@@ -32,9 +36,12 @@ const selectedPeer = computed(() => {
|
||||
let p = peers.Find(props.peerId)
|
||||
|
||||
if (!p) {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
if (!!props.peerId || props.peerId.length) {
|
||||
p = profile.peers.find((p) => p.Identifier === props.peerId)
|
||||
} else {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
|
||||
@@ -42,9 +49,13 @@ const selectedStats = computed(() => {
|
||||
let s = peers.Statistics(props.peerId)
|
||||
|
||||
if (!s) {
|
||||
s = freshStats() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
if (!!props.peerId || props.peerId.length) {
|
||||
s = profile.Statistics(props.peerId)
|
||||
} else {
|
||||
s = freshStats() // dummy stats to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
@@ -54,7 +65,6 @@ const selectedInterface = computed(() => {
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
@@ -69,29 +79,27 @@ const title = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const configStyle = ref("wgquick")
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
|
||||
configString.value = peers.configuration
|
||||
}
|
||||
}
|
||||
)
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
|
||||
configString.value = peers.configuration
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => configStyle.value, async () => {
|
||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier, configStyle.value)
|
||||
configString.value = peers.configuration
|
||||
})
|
||||
|
||||
function download() {
|
||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||
let filename = 'WireGuard-Tunnel.conf'
|
||||
if (selectedPeer.value.DisplayName) {
|
||||
filename = selectedPeer.value.DisplayName
|
||||
.replace(/ /g,"_")
|
||||
.replace(/[^a-zA-Z0-9-_]/g,"")
|
||||
.substring(0, 16)
|
||||
+ ".conf"
|
||||
}
|
||||
let text = configString.value
|
||||
|
||||
let element = document.createElement('a')
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', filename)
|
||||
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', selectedPeer.value.Filename)
|
||||
|
||||
element.style.display = 'none'
|
||||
document.body.appendChild(element)
|
||||
@@ -101,7 +109,7 @@ function download() {
|
||||
}
|
||||
|
||||
function email() {
|
||||
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => {
|
||||
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), configStyle.value, [selectedPeer.value.Identifier]).catch(e => {
|
||||
notify({
|
||||
title: "Failed to send mail with peer configuration!",
|
||||
text: e.toString(),
|
||||
@@ -110,33 +118,54 @@ function email() {
|
||||
})
|
||||
}
|
||||
|
||||
function ConfigQrUrl() {
|
||||
if (props.peerId.length) {
|
||||
return apiWrapper.url(`/peer/config-qr/${base64_url_encode(props.peerId)}?style=${configStyle.value}`)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<div class="d-flex justify-content-end align-items-center mb-1">
|
||||
<span class="me-2">{{ $t('modals.peer-view.style-label') }}: </span>
|
||||
<div class="btn-group btn-switch-group" role="group" aria-label="Configuration Style">
|
||||
<input type="radio" class="btn-check" name="configstyle" id="raw" value="raw" autocomplete="off" checked="" v-model="configStyle">
|
||||
<label class="btn btn-outline-dark btn-sm" for="raw">Raw</label>
|
||||
<input type="radio" class="btn-check" name="configstyle" id="wgquick" value="wgquick" autocomplete="off" checked="" v-model="configStyle">
|
||||
<label class="btn btn-outline-dark btn-sm" for="wgquick">WG-Quick</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion" id="peerInformation">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails" aria-expanded="true" aria-controls="collapseDetails">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails"
|
||||
aria-expanded="true" aria-controls="collapseDetails">
|
||||
{{ $t('modals.peer-view.section-info') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails" data-bs-parent="#peerInformation" style="">
|
||||
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails"
|
||||
data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li>
|
||||
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
||||
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip"
|
||||
class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
||||
<li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li>
|
||||
<li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li>
|
||||
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{ selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{ selectedPeer.DisabledReason }}</li>
|
||||
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{
|
||||
selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{
|
||||
selectedPeer.DisabledReason }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
|
||||
<img class="config-qr-img" :src="ConfigQrUrl()" loading="lazy" alt="Configuration QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,16 +173,20 @@ function email() {
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingStatus">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
|
||||
{{ $t('modals.peer-view.section-status') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus" data-bs-parent="#peerInformation" style="">
|
||||
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus"
|
||||
data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ $t('modals.peer-view.traffic') }}</h4>
|
||||
<p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{ selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up" :title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
|
||||
<p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{
|
||||
selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up"
|
||||
:title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
|
||||
<h4>{{ $t('modals.peer-view.connection-status') }}</h4>
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.pingable') }}: {{ selectedStats.IsPingable }}</li>
|
||||
@@ -166,13 +199,15 @@ function email() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
|
||||
<div v-if="selectedInterface.Mode === 'server'" class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingConfig">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
|
||||
{{ $t('modals.peer-view.section-config') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig" data-bs-parent="#peerInformation" style="">
|
||||
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig"
|
||||
data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<Prism language="ini" :code="configString"></Prism>
|
||||
</div>
|
||||
@@ -182,18 +217,25 @@ function email() {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-download') }}</button>
|
||||
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-email') }}</button>
|
||||
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{
|
||||
$t('modals.peer-view.button-download') }}</button>
|
||||
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{
|
||||
$t('modals.peer-view.button-email') }}</button>
|
||||
</div>
|
||||
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>
|
||||
|
||||
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
</template>
|
||||
</Modal></template>
|
||||
|
||||
<style>
|
||||
.config-qr-img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.btn-switch-group .btn {
|
||||
border-width: 1px;
|
||||
padding: 5px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,10 +5,12 @@ import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import {freshUser} from "@/helpers/models";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const users = userStore()
|
||||
const settings = settingsStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: String,
|
||||
@@ -32,6 +34,32 @@ const title = computed(() => {
|
||||
})
|
||||
|
||||
const formData = ref(freshUser())
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
|
||||
const passwordWeak = computed(() => {
|
||||
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
|
||||
})
|
||||
|
||||
const formValid = computed(() => {
|
||||
if (formData.value.Source !== 'db') {
|
||||
return true // nothing to validate
|
||||
}
|
||||
if (props.userId !== '#NEW#' && passwordWeak.value) {
|
||||
return false
|
||||
}
|
||||
if (props.userId === '#NEW#' && (!formData.value.Password || formData.value.Password.length < 1)) {
|
||||
return false
|
||||
}
|
||||
if (props.userId === '#NEW#' && passwordWeak.value) {
|
||||
return false
|
||||
}
|
||||
if (!formData.value.Identifier || formData.value.Identifier.length < 1) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
// functions
|
||||
|
||||
@@ -51,6 +79,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Notes = selectedUser.value.Notes
|
||||
formData.value.Password = ""
|
||||
formData.value.Disabled = selectedUser.value.Disabled
|
||||
formData.value.Locked = selectedUser.value.Locked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +91,8 @@ function close() {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (props.userId!=='#NEW#') {
|
||||
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
|
||||
@@ -75,10 +106,14 @@ async function save() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (isDeleting.value) return
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await users.DeleteUser(selectedUser.value.Identifier)
|
||||
close()
|
||||
@@ -88,6 +123,8 @@ async function del() {
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +145,8 @@ async function del() {
|
||||
</div>
|
||||
<div v-if="formData.Source==='db'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text">
|
||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :class="{ 'is-invalid': passwordWeak, 'is-valid': formData.Password !== '' && !passwordWeak }" :placeholder="$t('modals.user-edit.password.placeholder')" type="password">
|
||||
<div class="invalid-feedback">{{ $t('modals.user-edit.password.too-weak') }}</div>
|
||||
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -165,9 +203,15 @@ async function del() {
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid || isSaving">
|
||||
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.save') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
308
frontend/src/components/UserPeerEditModal.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import { peerStore } from "@/stores/peers";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { freshPeer, freshInterface } from '@/helpers/models';
|
||||
import { profileStore } from "@/stores/profile";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const profile = profileStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
let p = peers.Find(props.peerId)
|
||||
|
||||
if (!p) {
|
||||
if (!!props.peerId || props.peerId.length) {
|
||||
p = profile.peers.find((p) => p.Identifier === props.peerId)
|
||||
} else {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
}
|
||||
return p
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let iId = profile.selectedInterfaceId;
|
||||
|
||||
let i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
if (iId) {
|
||||
i = profile.interfaces.find((i) => i.Identifier === iId)
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (selectedPeer.value) {
|
||||
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
|
||||
}
|
||||
return t("modals.peer-edit.headline-new-peer")
|
||||
})
|
||||
|
||||
const formData = ref(freshPeer())
|
||||
const isSaving = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
if (!selectedPeer.value) {
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||
formData.value.Notes = peers.Prepared.Notes
|
||||
|
||||
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||
|
||||
formData.value.Mode = peers.Prepared.Mode
|
||||
|
||||
formData.value.Addresses = peers.Prepared.Addresses
|
||||
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||
formData.value.Dns = peers.Prepared.Dns
|
||||
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||
formData.value.Mtu = peers.Prepared.Mtu
|
||||
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = peers.Prepared.PreUp
|
||||
formData.value.PostUp = peers.Prepared.PostUp
|
||||
formData.value.PreDown = peers.Prepared.PreDown
|
||||
formData.value.PostDown = peers.Prepared.PostDown
|
||||
|
||||
} else { // fill existing data
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||
formData.value.Notes = selectedPeer.value.Notes
|
||||
|
||||
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||
|
||||
formData.value.Mode = selectedPeer.value.Mode
|
||||
|
||||
formData.value.Addresses = selectedPeer.value.Addresses
|
||||
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||
formData.value.Dns = selectedPeer.value.Dns
|
||||
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||
formData.value.Mtu = selectedPeer.value.Mtu
|
||||
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedPeer.value.PreUp
|
||||
formData.value.PostUp = selectedPeer.value.PostUp
|
||||
formData.value.PreDown = selectedPeer.value.PreDown
|
||||
formData.value.PostDown = selectedPeer.value.PostDown
|
||||
|
||||
if (!formData.value.Endpoint.Overridable ||
|
||||
!formData.value.EndpointPublicKey.Overridable ||
|
||||
!formData.value.AllowedIPs.Overridable ||
|
||||
!formData.value.PersistentKeepalive.Overridable ||
|
||||
!formData.value.Dns.Overridable ||
|
||||
!formData.value.DnsSearch.Overridable ||
|
||||
!formData.value.Mtu.Overridable ||
|
||||
!formData.value.FirewallMark.Overridable ||
|
||||
!formData.value.RoutingTable.Overridable ||
|
||||
!formData.value.PreUp.Overridable ||
|
||||
!formData.value.PostUp.Overridable ||
|
||||
!formData.value.PreDown.Overridable ||
|
||||
!formData.value.PostDown.Overridable) {
|
||||
formData.value.IgnoreGlobalSettings = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
|
||||
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||
formData.value.ExpiresAt = "" // reset expiry date
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshPeer()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (props.peerId !== '#NEW#') {
|
||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||
} else {
|
||||
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to save peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (isDeleting.value) return
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to delete peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')"
|
||||
v-model="formData.DisplayName">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||
v-model="formData.PrivateKey">
|
||||
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||
v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||
v-model="formData.PresharedKey">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')"
|
||||
v-model="formData.PersistentKeepalive.Value">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')"
|
||||
v-model="formData.Mtu.Value">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2"
|
||||
:placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||
<label class="form-check-label">{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
|
||||
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01"
|
||||
v-model="formData.ExpiresAt">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.peerId !== '#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del" :disabled="isDeleting">
|
||||
<span v-if="isDeleting" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="isSaving">
|
||||
<span v-if="isSaving" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
|
||||
{{ $t('general.save') }}
|
||||
</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style></style>
|
@@ -88,6 +88,10 @@ function close() {
|
||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||
<td>{{selectedUser.Department}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.api-enabled') }}:</td>
|
||||
<td>{{selectedUser.ApiEnabled}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Disabled">
|
||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||
<td>{{selectedUser.DisabledReason}}</td>
|
||||
|
@@ -5,6 +5,7 @@ export function freshInterface() {
|
||||
DisplayName: "",
|
||||
Identifier: "",
|
||||
Mode: "server",
|
||||
Backend: "local",
|
||||
|
||||
PublicKey: "",
|
||||
PrivateKey: "",
|
||||
@@ -42,7 +43,8 @@ export function freshInterface() {
|
||||
PeerDefPostDown: "",
|
||||
|
||||
TotalPeers: 0,
|
||||
EnabledPeers: 0
|
||||
EnabledPeers: 0,
|
||||
Filename: ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,8 +122,11 @@ export function freshPeer() {
|
||||
Overridable: true,
|
||||
},
|
||||
|
||||
// Internal value
|
||||
IgnoreGlobalSettings: false
|
||||
Filename: "",
|
||||
|
||||
// Internal values
|
||||
IgnoreGlobalSettings: false,
|
||||
IsSelected: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +151,12 @@ export function freshUser() {
|
||||
Locked: false,
|
||||
LockedReason: "",
|
||||
|
||||
PeerCount: 0
|
||||
ApiEnabled: false,
|
||||
|
||||
PeerCount: 0,
|
||||
|
||||
// Internal values
|
||||
IsSelected: false
|
||||
}
|
||||
}
|
||||
|
||||
|
20
frontend/src/helpers/utils.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Address4, Address6 } from "ip-address"
|
||||
|
||||
export function ipToBigInt(ip) {
|
||||
// Check if it's an IPv4 address
|
||||
if (ip.includes(".")) {
|
||||
const addr = new Address4(ip)
|
||||
return addr.bigInteger()
|
||||
}
|
||||
|
||||
// Otherwise, assume it's an IPv6 address
|
||||
const addr = new Address6(ip)
|
||||
return addr.bigInteger()
|
||||
}
|
||||
|
||||
export function humanFileSize(size) {
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||||
if (size === 0) return "0B"
|
||||
const i = parseInt(Math.floor(Math.log(size) / Math.log(1024)))
|
||||
return Math.round(size / Math.pow(1024, i), 2) + sizes[i]
|
||||
}
|
@@ -1,14 +1,26 @@
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
|
||||
export function validateCIDR(value) {
|
||||
return isCidr(value) !== 0
|
||||
export function validateCIDR() {
|
||||
return [{
|
||||
classes: 'invalid-cidr',
|
||||
rule: ({ text }) => isCidr(text) === 0,
|
||||
disableAdd: true,
|
||||
}]
|
||||
}
|
||||
|
||||
export function validateIP(value) {
|
||||
return isIP(value)
|
||||
export function validateIP() {
|
||||
return [{
|
||||
classes: 'invalid-ip',
|
||||
rule: ({ text }) => !isIP(text),
|
||||
disableAdd: true,
|
||||
}]
|
||||
}
|
||||
|
||||
export function validateDomain(value) {
|
||||
return true
|
||||
export function validateDomain() {
|
||||
return [{
|
||||
classes: 'invalid-domain',
|
||||
rule: tag => tag.text.length < 3,
|
||||
disableAdd: true,
|
||||
}]
|
||||
}
|
@@ -1,27 +1,38 @@
|
||||
// src/lang/index.js
|
||||
import de from './translations/de.json';
|
||||
import en from './translations/en.json';
|
||||
import {createI18n} from "vue-i18n";
|
||||
import fr from './translations/fr.json';
|
||||
import ko from './translations/ko.json';
|
||||
import pt from './translations/pt.json';
|
||||
import ru from './translations/ru.json';
|
||||
import uk from './translations/uk.json';
|
||||
import vi from './translations/vi.json';
|
||||
import zh from './translations/zh.json';
|
||||
|
||||
function getStoredLanguage() {
|
||||
let initialLang = localStorage.getItem('wgLang');
|
||||
if (!initialLang) {
|
||||
initialLang = "en"
|
||||
}
|
||||
return initialLang
|
||||
}
|
||||
import {createI18n} from "vue-i18n";
|
||||
|
||||
// Create i18n instance with options
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
allowComposition: true,
|
||||
locale: getStoredLanguage(), // set locale
|
||||
locale: (
|
||||
localStorage.getItem('wgLang')
|
||||
|| (window && window.navigator && (window.navigator.userLanguage || window.navigator.language).split('-')[0])
|
||||
|| 'en'
|
||||
), // set locale
|
||||
fallbackLocale: "en", // set fallback locale
|
||||
messages: {
|
||||
"de": de,
|
||||
"en": en
|
||||
"en": en,
|
||||
"fr": fr,
|
||||
"ko": ko,
|
||||
"pt": pt,
|
||||
"ru": ru,
|
||||
"uk": uk,
|
||||
"vi": vi,
|
||||
"zh": zh,
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n
|
||||
export default i18n
|
||||
|
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"languages": {
|
||||
"de": "Deutsch"
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Anzahl an Elementen",
|
||||
@@ -23,10 +26,11 @@
|
||||
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
|
||||
},
|
||||
"password": {
|
||||
"label": "Kennwort",
|
||||
"label": "Passwort",
|
||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||
},
|
||||
"button": "Anmelden"
|
||||
"button": "Anmelden",
|
||||
"button-webauthn": "Passkey verwenden"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
@@ -34,8 +38,11 @@
|
||||
"users": "Benutzer",
|
||||
"lang": "Sprache ändern",
|
||||
"profile": "Mein Profil",
|
||||
"settings": "Einstellungen",
|
||||
"audit": "Event Protokoll",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
"logout": "Abmelden",
|
||||
"keygen": "Schlüsselgenerator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -45,7 +52,7 @@
|
||||
"box-header": "WireGuard Installation",
|
||||
"headline": "Installation",
|
||||
"content": "Die Installationsanweisungen für die Client-Software finden Sie auf der offiziellen WireGuard-Website.",
|
||||
"btn": "Anleitung öffnen"
|
||||
"button": "Anleitung öffnen"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "Über WireGuard",
|
||||
@@ -75,77 +82,79 @@
|
||||
},
|
||||
"interfaces": {
|
||||
"headline": "Schnittstellenverwaltung",
|
||||
"headline-peers": "Current VPN Peers",
|
||||
"headline-endpoints": "Current Endpoints",
|
||||
"headline-peers": "Aktuelle VPN-Peers",
|
||||
"headline-endpoints": "Aktuelle Endpunkte",
|
||||
"no-interface": {
|
||||
"default-selection": "No Interface available",
|
||||
"headline": "No interfaces found...",
|
||||
"abstract": "Click the plus button above to create a new WireGuard interface."
|
||||
"default-selection": "Keine Schnittstelle verfügbar",
|
||||
"headline": "Keine Schnittstellen gefunden...",
|
||||
"abstract": "Klicken Sie auf die Plus-Schaltfläche oben, um eine neue WireGuard-Schnittstelle zu erstellen."
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
|
||||
"headline": "Keine Peers verfügbar",
|
||||
"abstract": "Derzeit sind keine Peers für die ausgewählte WireGuard-Schnittstelle verfügbar."
|
||||
},
|
||||
"table-heading": {
|
||||
"name": "Name",
|
||||
"user": "User",
|
||||
"user": "Benutzer",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"endpoint": "Endpunkt",
|
||||
"status": "Status"
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Interface status for",
|
||||
"mode": "mode",
|
||||
"key": "Public Key",
|
||||
"endpoint": "Public Endpoint",
|
||||
"port": "Listening Port",
|
||||
"peers": "Enabled Peers",
|
||||
"total-peers": "Total Peers",
|
||||
"endpoints": "Enabled Endpoints",
|
||||
"total-endpoints": "Total Endpoints",
|
||||
"ip": "IP Address",
|
||||
"default-allowed-ip": "Default allowed IPs",
|
||||
"dns": "DNS Servers",
|
||||
"headline": "Schnittstellenstatus für",
|
||||
"backend": "Backend",
|
||||
"unknown-backend": "Unbekannt",
|
||||
"wrong-backend": "Ungültiges Backend, das lokale WireGuard Backend wird stattdessen verwendet!",
|
||||
"key": "Öffentlicher Schlüssel",
|
||||
"endpoint": "Öffentlicher Endpunkt",
|
||||
"port": "Port",
|
||||
"peers": "Aktive Peers",
|
||||
"total-peers": "Gesamtanzahl Peers",
|
||||
"endpoints": "Aktive Endpunkte",
|
||||
"total-endpoints": "Gesamtanzahl Endpunkte",
|
||||
"ip": "IP-Adresse",
|
||||
"default-allowed-ip": "Standard Erlaubte-IPs",
|
||||
"dns": "DNS-Server",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Default Keepalive Interval",
|
||||
"button-show-config": "Show configuration",
|
||||
"button-download-config": "Download configuration",
|
||||
"button-store-config": "Store configuration for wg-quick",
|
||||
"button-edit": "Edit interface"
|
||||
"default-keep-alive": "Standard Keepalive-Intervall",
|
||||
"button-show-config": "Konfiguration anzeigen",
|
||||
"button-download-config": "Konfiguration herunterladen",
|
||||
"button-store-config": "Konfiguration für wg-quick speichern",
|
||||
"button-edit": "Schnittstelle bearbeiten"
|
||||
},
|
||||
"button-add-interface": "Add Interface",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-add-peers": "Add Multiple Peers",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer",
|
||||
"peer-disabled": "Peer is disabled, reason:",
|
||||
"peer-expiring": "Peer is expiring at",
|
||||
"peer-connected": "Connected",
|
||||
"peer-not-connected": "Not Connected",
|
||||
"peer-handshake": "Last handshake:"
|
||||
"button-add-interface": "Schnittstelle hinzufügen",
|
||||
"button-add-peer": "Peer hinzufügen",
|
||||
"button-add-peers": "Mehrere Peers hinzufügen",
|
||||
"button-show-peer": "Peer anzeigen",
|
||||
"button-edit-peer": "Peer bearbeiten",
|
||||
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
||||
"peer-expiring": "Peer läuft ab am",
|
||||
"peer-connected": "Verbunden",
|
||||
"peer-not-connected": "Nicht verbunden",
|
||||
"peer-handshake": "Letzter Handshake:"
|
||||
},
|
||||
"users": {
|
||||
"headline": "Benutzerverwaltung",
|
||||
"table-heading": {
|
||||
"id": "ID",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"source": "Source",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"source": "Quelle",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"no-user": {
|
||||
"headline": "No users available",
|
||||
"abstract": "Currently, there are no users registered with WireGuard Portal."
|
||||
"headline": "Keine Benutzer verfügbar",
|
||||
"abstract": "Derzeit sind keine Benutzer im WireGuard-Portal registriert."
|
||||
},
|
||||
"button-add-user": "Add User",
|
||||
"button-show-user": "Show User",
|
||||
"button-edit-user": "Edit User",
|
||||
"user-disabled": "User is disabled, reason:",
|
||||
"user-locked": "Account is locked, reason:",
|
||||
"admin": "User has administrator privileges",
|
||||
"no-admin": "User has no administrator privileges"
|
||||
"button-add-user": "Benutzer hinzufügen",
|
||||
"button-show-user": "Benutzer anzeigen",
|
||||
"button-edit-user": "Benutzer bearbeiten",
|
||||
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
||||
"user-locked": "Konto ist gesperrt, Grund:",
|
||||
"admin": "Benutzer hat Administratorrechte",
|
||||
"no-admin": "Benutzer hat keine Administratorrechte"
|
||||
},
|
||||
"profile": {
|
||||
"headline": "Meine VPN-Konfigurationen",
|
||||
@@ -153,337 +162,430 @@
|
||||
"name": "Name",
|
||||
"ip": "IP's",
|
||||
"stats": "Status",
|
||||
"interface": "Server Interface"
|
||||
"interface": "Server-Schnittstelle"
|
||||
},
|
||||
"no-peer": {
|
||||
"headline": "No peers available",
|
||||
"abstract": "Currently, there are no peers associated with your user profile."
|
||||
"headline": "Keine Peers verfügbar",
|
||||
"abstract": "Derzeit sind keine Peers mit Ihrem Benutzerprofil verknüpft."
|
||||
},
|
||||
"peer-connected": "Connected",
|
||||
"button-add-peer": "Add Peer",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
"peer-connected": "Verbunden",
|
||||
"button-add-peer": "Peer hinzufügen",
|
||||
"button-show-peer": "Peer anzeigen",
|
||||
"button-edit-peer": "Peer bearbeiten"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Einstellungen",
|
||||
"abstract": "Hier finden Sie persönliche Einstellungen für WireGuard Portal.",
|
||||
"api": {
|
||||
"headline": "API Einstellungen",
|
||||
"abstract": "Hier können Sie die RESTful API verwalten.",
|
||||
"active-description": "Die API ist derzeit für Ihr Benutzerkonto aktiv. Alle API-Anfragen werden mit Basic Auth authentifiziert. Verwenden Sie zur Authentifizierung die folgenden Anmeldeinformationen.",
|
||||
"inactive-description": "Die API ist derzeit inaktiv. Klicken Sie auf die Schaltfläche unten, um sie zu aktivieren.",
|
||||
"user-label": "API Benutzername:",
|
||||
"user-placeholder": "API Benutzer",
|
||||
"token-label": "API Passwort:",
|
||||
"token-placeholder": "API Token",
|
||||
"token-created-label": "API-Zugriff gewährt seit: ",
|
||||
"button-disable-title": "Deaktivieren Sie die API. Dadurch wird der aktuelle Token ungültig.",
|
||||
"button-disable-text": "API deaktivieren",
|
||||
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||
"button-enable-text": "API aktivieren",
|
||||
"api-link": "API Dokumentation"
|
||||
},
|
||||
"webauthn": {
|
||||
"headline": "Passkey-Einstellungen",
|
||||
"abstract": "Passkeys sind eine moderne Möglichkeit, Benutzer ohne Passwort zu authentifizieren. Sie werden sicher in Ihrem Browser gespeichert und können verwendet werden, um sich im WireGuard-Portal anzumelden.",
|
||||
"active-description": "Mindestens ein Passkey ist derzeit für Ihr Benutzerkonto aktiv.",
|
||||
"inactive-description": "Für Ihr Benutzerkonto sind derzeit keine Passkeys registriert. Drücken Sie die Schaltfläche unten, um einen neuen Passkey zu registrieren.",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"created": "Erstellt",
|
||||
"actions": ""
|
||||
},
|
||||
"credentials-list": "Derzeit registrierte Passkeys",
|
||||
"modal-delete": {
|
||||
"headline": "Passkey löschen",
|
||||
"abstract": "Sind Sie sicher, dass Sie diesen Passkey löschen möchten? Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||
"created": "Erstellt:",
|
||||
"button-delete": "Löschen",
|
||||
"button-cancel": "Abbrechen"
|
||||
},
|
||||
"button-rename-title": "Umbenennen",
|
||||
"button-rename-text": "Passkey umbenennen.",
|
||||
"button-save-title": "Speichern",
|
||||
"button-save-text": "Neuen Namen des Passkeys speichern.",
|
||||
"button-cancel-title": "Abbrechen",
|
||||
"button-cancel-text": "Umbenennung des Passkeys abbrechen.",
|
||||
"button-delete-title": "Löschen",
|
||||
"button-delete-text": "Passkey löschen. Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||
"button-register-title": "Passkey registrieren",
|
||||
"button-register-text": "Einen neuen Passkey registrieren, um Ihr Konto zu sichern."
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"headline": "Eventprotokoll",
|
||||
"abstract": "Hier finden Sie das Eventprotokoll aller im WireGuard-Portal vorgenommenen Aktionen.",
|
||||
"no-entries": {
|
||||
"headline": "Keine Protokolleinträge verfügbar",
|
||||
"abstract": "Derzeit sind keine Eventprotokolle aufgezeichnet."
|
||||
},
|
||||
"entries-headline": "Protokolleinträge",
|
||||
"table-heading": {
|
||||
"id": "#",
|
||||
"time": "Zeit",
|
||||
"user": "Benutzer",
|
||||
"severity": "Schweregrad",
|
||||
"origin": "Ursprung",
|
||||
"message": "Nachricht"
|
||||
}
|
||||
},
|
||||
"keygen": {
|
||||
"headline": "WireGuard Key Generator",
|
||||
"abstract": "Hier können Sie WireGuard Schlüsselpaare generieren. Die Schlüssel werden lokal auf Ihrem Computer generiert und niemals an den Server gesendet.",
|
||||
"headline-keypair": "Neues Schlüsselpaar",
|
||||
"headline-preshared-key": "Neuer Pre-Shared Key",
|
||||
"button-generate": "Erzeugen",
|
||||
"private-key": {
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Pre-Shared Key",
|
||||
"placeholder": "Der geteilte Schlüssel"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
"tab-user": "Information",
|
||||
"headline": "Benutzerkonto:",
|
||||
"tab-user": "Informationen",
|
||||
"tab-peers": "Peers",
|
||||
"headline-info": "User Information:",
|
||||
"headline-notes": "Notes:",
|
||||
"headline-info": "Benutzerinformationen:",
|
||||
"headline-notes": "Notizen:",
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"department": "Department",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"phone": "Telefonnummer",
|
||||
"department": "Abteilung",
|
||||
"api-enabled": "API-Zugriff",
|
||||
"disabled": "Konto deaktiviert",
|
||||
"locked": "Konto gesperrt",
|
||||
"no-peers": "Benutzer hat keine zugeordneten Peers.",
|
||||
"peers": {
|
||||
"name": "Name",
|
||||
"interface": "Interface",
|
||||
"interface": "Schnittstelle",
|
||||
"ip": "IP's"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
"headline-edit": "Edit user:",
|
||||
"headline-new": "New user",
|
||||
"header-general": "General",
|
||||
"header-personal": "User Information",
|
||||
"header-notes": "Notes",
|
||||
"header-state": "State",
|
||||
"headline-edit": "Benutzer bearbeiten:",
|
||||
"headline-new": "Neuer Benutzer",
|
||||
"header-general": "Allgemein",
|
||||
"header-personal": "Benutzerinformationen",
|
||||
"header-notes": "Notizen",
|
||||
"header-state": "Status",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique user identifier"
|
||||
"label": "Kennung",
|
||||
"placeholder": "Die eindeutige Benutzerkennung"
|
||||
},
|
||||
"source": {
|
||||
"label": "Source",
|
||||
"placeholder": "The user source"
|
||||
"label": "Quelle",
|
||||
"placeholder": "Die Benutzerquelle"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "A super secret password",
|
||||
"description": "Leave this field blank to keep current password."
|
||||
"label": "Passwort",
|
||||
"placeholder": "Ein super geheimes Passwort",
|
||||
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten.",
|
||||
"too-weak": "Das Passwort entspricht nicht den Sicherheitsanforderungen."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
"placeholder": "The email address"
|
||||
"label": "E-Mail",
|
||||
"placeholder": "Die E-Mail-Adresse"
|
||||
},
|
||||
"phone": {
|
||||
"label": "Phone",
|
||||
"placeholder": "The phone number"
|
||||
"label": "Telefon",
|
||||
"placeholder": "Die Telefonnummer"
|
||||
},
|
||||
"department": {
|
||||
"label": "Department",
|
||||
"placeholder": "The department"
|
||||
"label": "Abteilung",
|
||||
"placeholder": "Die Abteilung"
|
||||
},
|
||||
"firstname": {
|
||||
"label": "Firstname",
|
||||
"placeholder": "Firstname"
|
||||
"label": "Vorname",
|
||||
"placeholder": "Vorname"
|
||||
},
|
||||
"lastname": {
|
||||
"label": "Lastname",
|
||||
"placeholder": "Lastname"
|
||||
"label": "Nachname",
|
||||
"placeholder": "Nachname"
|
||||
},
|
||||
"notes": {
|
||||
"label": "Notes",
|
||||
"label": "Notizen",
|
||||
"placeholder": ""
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Disabled (no WireGuard connection and no login possible)"
|
||||
"label": "Deaktiviert (keine WireGuard-Verbindung und kein Login möglich)"
|
||||
},
|
||||
"locked": {
|
||||
"label": "Locked (no login possible, WireGuard connections still work)"
|
||||
"label": "Gesperrt (kein Login möglich, WireGuard-Verbindungen funktionieren weiterhin)"
|
||||
},
|
||||
"admin": {
|
||||
"label": "Is Admin"
|
||||
"label": "Ist Administrator"
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Config for Interface:"
|
||||
"headline": "Konfiguration für Schnittstelle:"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Edit Interface:",
|
||||
"headline-new": "New Interface",
|
||||
"tab-interface": "Interface",
|
||||
"tab-peerdef": "Peer Defaults",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Interface Hooks",
|
||||
"headline-edit": "Schnittstelle bearbeiten:",
|
||||
"headline-new": "Neue Schnittstelle",
|
||||
"tab-interface": "Schnittstelle",
|
||||
"tab-peerdef": "Peer-Standardeinstellungen",
|
||||
"header-general": "Allgemein",
|
||||
"header-network": "Netzwerk",
|
||||
"header-crypto": "Kryptografie",
|
||||
"header-hooks": "Schnittstellen-Hooks",
|
||||
"header-peer-hooks": "Hooks",
|
||||
"header-state": "State",
|
||||
"header-state": "Status",
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "The unique interface identifier"
|
||||
"label": "Kennung",
|
||||
"placeholder": "Die eindeutige Schnittstellenkennung"
|
||||
},
|
||||
"mode": {
|
||||
"label": "Interface Mode",
|
||||
"server": "Server Mode",
|
||||
"client": "Client Mode",
|
||||
"any": "Unknown Mode"
|
||||
"label": "Schnittstellenmodus",
|
||||
"server": "Server-Modus",
|
||||
"client": "Client-Modus",
|
||||
"any": "Unbekannter Modus"
|
||||
},
|
||||
"backend": {
|
||||
"label": "Schnittstellenbackend",
|
||||
"invalid-label": "Ursprüngliches Backend ist ungültig, das lokale WireGuard Backend wird stattdessen verwendet!",
|
||||
"local": "Lokales WireGuard Backend"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the interface"
|
||||
"label": "Anzeigename",
|
||||
"placeholder": "Der beschreibende Name für die Schnittstelle"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
"label": "IP-Adressen",
|
||||
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"listen-port": {
|
||||
"label": "Listen Port",
|
||||
"placeholder": "The listening port"
|
||||
"label": "Port",
|
||||
"placeholder": "Der Port der WireGuard Schnittstelle"
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
"label": "DNS-Server",
|
||||
"placeholder": "Die zu verwendenden DNS-Server"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
"label": "DNS-Suchdomänen",
|
||||
"placeholder": "DNS-Suchpräfixe"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The interface MTU (0 = keep default)"
|
||||
"placeholder": "Die Schnittstellen-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"firewall-mark": {
|
||||
"label": "Firewall Mark",
|
||||
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
|
||||
"label": "Firewall-Markierung",
|
||||
"placeholder": "Firewall-Markierung, die auf ausgehenden Datenverkehr angewendet wird. (0 = automatisch)"
|
||||
},
|
||||
"routing-table": {
|
||||
"label": "Routing Table",
|
||||
"placeholder": "The routing table ID",
|
||||
"description": "Special cases: off = do not manage routes, 0 = automatic"
|
||||
"label": "Routing-Tabelle",
|
||||
"placeholder": "Die Routing-Tabellen-ID",
|
||||
"description": "Spezialfälle: off = Routen nicht verwalten, 0 = automatisch"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
"label": "Schnittstelle deaktiviert"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
"label": "wg-quick Konfiguration automatisch speichern"
|
||||
},
|
||||
"defaults": {
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "Endpoint Address",
|
||||
"description": "The endpoint address that peers will connect to."
|
||||
"label": "Endpunktadresse",
|
||||
"placeholder": "Endpunktadresse",
|
||||
"description": "Die Endpunktadresse, mit der sich Peers verbinden. (z.B. wg.example.com oder wg.example.com:51820)"
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP Networks",
|
||||
"placeholder": "Network Addresses",
|
||||
"description": "Peers will get IP addresses from those subnets."
|
||||
"label": "IP-Netzwerke",
|
||||
"placeholder": "Netzwerkadressen",
|
||||
"description": "Peers erhalten IP-Adressen aus diesen Subnetzen."
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Default Allowed IP Addresses"
|
||||
"label": "Erlaubte IP-Adressen",
|
||||
"placeholder": "Erlaubte IP-Adressen für Peers"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
"label": "Keepalive-Intervall",
|
||||
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||
}
|
||||
},
|
||||
|
||||
"button-apply-defaults": "Apply Peer Defaults"
|
||||
"button-apply-defaults": "Peer-Standardeinstellungen anwenden"
|
||||
},
|
||||
"peer-view": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"section-info": "Peer Information",
|
||||
"section-status": "Current Status",
|
||||
"section-config": "Configuration",
|
||||
"identifier": "Identifier",
|
||||
"ip": "IP Addresses",
|
||||
"user": "Associated User",
|
||||
"notes": "Notes",
|
||||
"expiry-status": "Expires At",
|
||||
"disabled-status": "Disabled At",
|
||||
"traffic": "Traffic",
|
||||
"connection-status": "Connection Stats",
|
||||
"upload": "Uploaded Bytes (from Server to Peer)",
|
||||
"download": "Downloaded Bytes (from Peer to Server)",
|
||||
"pingable": "Is Pingable",
|
||||
"handshake": "Last Handshake",
|
||||
"connected-since": "Connected since",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Download configuration",
|
||||
"button-email": "Send configuration via E-Mail"
|
||||
"headline-endpoint": "Endpunkt:",
|
||||
"section-info": "Peer-Informationen",
|
||||
"section-status": "Aktueller Status",
|
||||
"section-config": "Konfiguration",
|
||||
"identifier": "Kennung",
|
||||
"ip": "IP-Adressen",
|
||||
"user": "Zugeordneter Benutzer",
|
||||
"notes": "Notizen",
|
||||
"expiry-status": "Läuft ab am",
|
||||
"disabled-status": "Deaktiviert am",
|
||||
"traffic": "Datenverkehr",
|
||||
"connection-status": "Verbindungsstatistiken",
|
||||
"upload": "Hochgeladene Bytes (vom Server zum Peer)",
|
||||
"download": "Heruntergeladene Bytes (vom Peer zum Server)",
|
||||
"pingable": "Pingbar",
|
||||
"handshake": "Letzter Handshake",
|
||||
"connected-since": "Verbunden seit",
|
||||
"endpoint": "Endpunkt",
|
||||
"button-download": "Konfiguration herunterladen",
|
||||
"button-email": "Konfiguration per E-Mail senden",
|
||||
"style-label": "Konfigurationsformat"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Edit peer:",
|
||||
"headline-edit-endpoint": "Edit endpoint:",
|
||||
"headline-new-peer": "Create peer",
|
||||
"headline-new-endpoint": "Create endpoint",
|
||||
"header-general": "General",
|
||||
"header-network": "Network",
|
||||
"header-crypto": "Cryptography",
|
||||
"header-hooks": "Hooks (Executed on Peer)",
|
||||
"header-state": "State",
|
||||
"headline-edit-peer": "Peer bearbeiten:",
|
||||
"headline-edit-endpoint": "Endpunkt bearbeiten:",
|
||||
"headline-new-peer": "Peer erstellen",
|
||||
"headline-new-endpoint": "Endpunkt erstellen",
|
||||
"header-general": "Allgemein",
|
||||
"header-network": "Netzwerk",
|
||||
"header-crypto": "Kryptografie",
|
||||
"header-hooks": "Hooks (beim Peer ausgeführt)",
|
||||
"header-state": "Status",
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the peer"
|
||||
"label": "Anzeigename",
|
||||
"placeholder": "Der beschreibende Name für den Peer"
|
||||
},
|
||||
"linked-user": {
|
||||
"label": "Linked User",
|
||||
"placeholder": "The user account which owns this peer"
|
||||
"label": "Verknüpfter Benutzer",
|
||||
"placeholder": "Das Benutzerkonto, dem dieser Peer gehört"
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"label": "Privater Schlüssel",
|
||||
"placeholder": "Der private Schlüssel",
|
||||
"help": "Der private Schlüssel wird sicher auf dem Server gespeichert. Wenn der Benutzer bereits eine Kopie besitzt, kann dieses Feld entfallen. Der Server funktioniert auch ausschließlich mit dem öffentlichen Schlüssel des Peers."
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
"label": "Öffentlicher Schlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "Optional pre-shared key"
|
||||
"label": "Pre-Shared Key",
|
||||
"placeholder": "Optionaler geteilter Schlüssel"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Endpoint public Key",
|
||||
"placeholder": "The public key of the remote endpoint"
|
||||
"label": "Öffentlicher Endpunktschlüssel",
|
||||
"placeholder": "Der öffentliche Schlüssel des entfernten Endpunkts"
|
||||
},
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "The address of the remote endpoint"
|
||||
"label": "Endpunktadresse",
|
||||
"placeholder": "Die Adresse des entfernten Endpunkts"
|
||||
},
|
||||
"ip": {
|
||||
"label": "IP Addresses",
|
||||
"placeholder": "IP Addresses (CIDR format)"
|
||||
"label": "IP-Adressen",
|
||||
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IP Addresses",
|
||||
"placeholder": "Allowed IP Addresses (CIDR format)"
|
||||
"label": "Erlaubte IP-Adressen",
|
||||
"placeholder": "Erlaubte IP-Adressen (CIDR-Format)"
|
||||
},
|
||||
"extra-allowed-ip": {
|
||||
"label": "Extra allowed IP Addresses",
|
||||
"placeholder": "Extra allowed IP's (Server Sided)",
|
||||
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
|
||||
"label": "Zusätzliche erlaubte IP-Adressen",
|
||||
"placeholder": "Zusätzliche erlaubte IP's (Server-seitig)",
|
||||
"description": "Diese IPs werden an der entfernten WireGuard-Schnittstelle als erlaubte IPs hinzugefügt."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
"label": "DNS-Server",
|
||||
"placeholder": "Die zu verwendenden DNS-Server"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
"label": "DNS-Suchdomänen",
|
||||
"placeholder": "DNS-Suchpräfixe"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
"label": "Keepalive-Intervall",
|
||||
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Peer Disabled"
|
||||
"label": "Peer deaktiviert"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Ignore global settings"
|
||||
"label": "Globale Einstellungen ignorieren"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Expiry date"
|
||||
"label": "Ablaufdatum"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Create multiple peers",
|
||||
"headline-endpoint": "Create multiple endpoints",
|
||||
"headline-peer": "Mehrere Peers erstellen",
|
||||
"headline-endpoint": "Mehrere Endpunkte erstellen",
|
||||
"identifiers": {
|
||||
"label": "User Identifiers",
|
||||
"placeholder": "User Identifiers",
|
||||
"description": "A user identifier (the username) for which a peer should be created."
|
||||
"label": "Benutzerkennungen",
|
||||
"placeholder": "Benutzerkennungen",
|
||||
"description": "Eine Benutzerkennung (der Benutzername), für die ein Peer erstellt werden soll."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Display Name Prefix",
|
||||
"placeholder": "The prefix",
|
||||
"description": "A prefix that is added to the peers display name."
|
||||
"headline-endpoint": "Endpunkt:",
|
||||
"label": "Anzeigename-Präfix",
|
||||
"placeholder": "Das Präfix",
|
||||
"description": "Ein Präfix, das dem Anzeigenamen des Peers hinzugefügt wird."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"languages": {
|
||||
"en": "English"
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Number of Elements",
|
||||
@@ -26,7 +29,8 @@
|
||||
"label": "Password",
|
||||
"placeholder": "Please enter your password"
|
||||
},
|
||||
"button": "Sign in"
|
||||
"button": "Sign in",
|
||||
"button-webauthn": "Use Passkey"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
@@ -34,8 +38,11 @@
|
||||
"users": "Users",
|
||||
"lang": "Toggle Language",
|
||||
"profile": "My Profile",
|
||||
"settings": "Settings",
|
||||
"audit": "Audit Log",
|
||||
"login": "Login",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"keygen": "Key Generator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -45,7 +52,7 @@
|
||||
"box-header": "WireGuard Installation",
|
||||
"headline": "Installation",
|
||||
"content": "Installation instructions for client software can be found on the official WireGuard website.",
|
||||
"btn": "Open Instructions"
|
||||
"button": "Open Instructions"
|
||||
},
|
||||
"about-wg": {
|
||||
"box-header": "About WireGuard",
|
||||
@@ -95,7 +102,9 @@
|
||||
},
|
||||
"interface": {
|
||||
"headline": "Interface status for",
|
||||
"mode": "mode",
|
||||
"backend": "Backend",
|
||||
"unknown-backend": "Unknown",
|
||||
"wrong-backend": "Invalid backend, using local WireGuard backend instead!",
|
||||
"key": "Public Key",
|
||||
"endpoint": "Public Endpoint",
|
||||
"port": "Listening Port",
|
||||
@@ -164,6 +173,91 @@
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer"
|
||||
},
|
||||
"settings": {
|
||||
"headline": "Settings",
|
||||
"abstract": "Here you can change your personal settings.",
|
||||
"api": {
|
||||
"headline": "API Settings",
|
||||
"abstract": "Here you can configure the RESTful API settings.",
|
||||
"active-description": "The API is currently active for your user account. All API requests are authenticated with Basic Auth. Use the following credentials for authentication.",
|
||||
"inactive-description": "The API is currently inactive. Press the button below to activate it.",
|
||||
"user-label": "API Username:",
|
||||
"user-placeholder": "The API user",
|
||||
"token-label": "API Password:",
|
||||
"token-placeholder": "The API token",
|
||||
"token-created-label": "API access granted at: ",
|
||||
"button-disable-title": "Disable API, this will invalidate the current token.",
|
||||
"button-disable-text": "Disable API",
|
||||
"button-enable-title": "Enable API, this will generate a new token.",
|
||||
"button-enable-text": "Enable API",
|
||||
"api-link": "API Documentation"
|
||||
},
|
||||
"webauthn": {
|
||||
"headline": "Passkey Settings",
|
||||
"abstract": "Passkeys are a modern way to authenticate users without the need for passwords. They are stored securely in your browser and can be used to log in to the WireGuard Portal.",
|
||||
"active-description": "At least one passkey is currently active for your user account.",
|
||||
"inactive-description": "No passkeys are currently registered for your user account. Press the button below to register a new passkey.",
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"created": "Created",
|
||||
"actions": ""
|
||||
},
|
||||
"credentials-list": "Currently registered Passkeys",
|
||||
"modal-delete": {
|
||||
"headline": "Delete Passkey",
|
||||
"abstract": "Are you sure you want to delete this passkey? You will not be able to log in with this passkey anymore.",
|
||||
"created": "Created:",
|
||||
"button-delete": "Delete",
|
||||
"button-cancel": "Cancel"
|
||||
},
|
||||
"button-rename-title": "Rename",
|
||||
"button-rename-text": "Rename the passkey.",
|
||||
"button-save-title": "Save",
|
||||
"button-save-text": "Save the new name of the passkey.",
|
||||
"button-cancel-title": "Cancel",
|
||||
"button-cancel-text": "Cancel the renaming of the passkey.",
|
||||
"button-delete-title": "Delete",
|
||||
"button-delete-text": "Delete the passkey. You will not be able to log in with this passkey anymore.",
|
||||
"button-register-title": "Register Passkey",
|
||||
"button-register-text": "Register a new Passkey to secure your account."
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"headline": "Audit Log",
|
||||
"abstract": "Here you can find the audit log of all actions performed in the WireGuard Portal.",
|
||||
"no-entries": {
|
||||
"headline": "No log entries available",
|
||||
"abstract": "Currently, there are no audit logs recorded."
|
||||
},
|
||||
"entries-headline": "Log Entries",
|
||||
"table-heading": {
|
||||
"id": "#",
|
||||
"time": "Time",
|
||||
"user": "User",
|
||||
"severity": "Severity",
|
||||
"origin": "Origin",
|
||||
"message": "Message"
|
||||
}
|
||||
},
|
||||
"keygen": {
|
||||
"headline": "WireGuard Key Generator",
|
||||
"abstract": "Generate a new WireGuard keys. The keys are generated in your local browser and are never sent to the server.",
|
||||
"headline-keypair": "New Key Pair",
|
||||
"headline-preshared-key": "New Preshared Key",
|
||||
"button-generate": "Generate",
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
"placeholder": "The public key"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Preshared Key",
|
||||
"placeholder": "The pre-shared key"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
@@ -174,8 +268,9 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"phone": "Phone number",
|
||||
"phone": "Phone Number",
|
||||
"department": "Department",
|
||||
"api-enabled": "API Access",
|
||||
"disabled": "Account Disabled",
|
||||
"locked": "Account Locked",
|
||||
"no-peers": "User has no associated peers.",
|
||||
@@ -203,7 +298,8 @@
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"placeholder": "A super secret password",
|
||||
"description": "Leave this field blank to keep current password."
|
||||
"description": "Leave this field blank to keep current password.",
|
||||
"too-weak": "The password is too weak. Please use a stronger password."
|
||||
},
|
||||
"email": {
|
||||
"label": "Email",
|
||||
@@ -263,6 +359,11 @@
|
||||
"client": "Client Mode",
|
||||
"any": "Unknown Mode"
|
||||
},
|
||||
"backend": {
|
||||
"label": "Interface Backend",
|
||||
"invalid-label": "Original backend is no longer available, using local WireGuard backend instead!",
|
||||
"local": "Local WireGuard Backend"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Display Name",
|
||||
"placeholder": "The descriptive name for the interface"
|
||||
@@ -330,7 +431,7 @@
|
||||
"endpoint": {
|
||||
"label": "Endpoint Address",
|
||||
"placeholder": "Endpoint Address",
|
||||
"description": "The endpoint address that peers will connect to."
|
||||
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
|
||||
},
|
||||
"networks": {
|
||||
"label": "IP Networks",
|
||||
@@ -374,7 +475,8 @@
|
||||
"connected-since": "Connected since",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Download configuration",
|
||||
"button-email": "Send configuration via E-Mail"
|
||||
"button-email": "Send configuration via E-Mail",
|
||||
"style-label": "Configuration Style"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Edit peer:",
|
||||
@@ -396,7 +498,8 @@
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Private Key",
|
||||
"placeholder": "The private key"
|
||||
"placeholder": "The private key",
|
||||
"help": "The private key is stored securely on the server. If the user already holds a copy, you may omit this field. The server still functions exclusively with the peer’s public key."
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Public Key",
|
||||
|