Compare commits

..

7 Commits

Author SHA1 Message Date
onyx-flame
e6b01a9903 Feature (v1): add latest handshake data to API response (#203)
* feature: updated handshake-related fields type

* feature: updated handshake representations in templates

* feature: added handshake field to Swagger schema
2023-12-23 12:56:52 +01:00
Christoph Haas
2f79dd04c0 adopt github actions 2023-10-26 11:29:34 +02:00
Christoph Haas
e5ed9736b3 update docker build settings, move to new docker hub repository, use stable branch and major version tags 2023-10-26 11:22:58 +02:00
Christoph Haas
c8353b85ae Merge branch 'replace_ext_lib' into stable 2023-10-26 10:40:06 +02:00
Christoph Haas
6142031387 update gin 2023-10-26 10:39:01 +02:00
Christoph Haas
dd86d0ff49 replace inaccessible external lib 2023-10-26 10:31:29 +02:00
Christoph Haas
bdd426a679 populate peer device type (#170) 2023-10-26 10:20:08 +02:00
438 changed files with 11808 additions and 40964 deletions

View File

@@ -7,10 +7,6 @@ jobs:
- restore_cache:
keys:
- go-mod-latest-v4-{{ checksum "go.sum" }}
- run:
name: Build Frontend
command: |
make frontend
- run:
name: Install Dependencies
command: |
@@ -55,13 +51,42 @@ jobs:
fi
working_directory: ~/repo
docker:
- image: cimg/go:1.21-node
- image: cimg/go:1.19
build-118: # just to validate compatibility with minimum go version
steps:
- checkout
- restore_cache:
keys:
- go-mod-118-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
make build-dependencies
- save_cache:
key: go-mod-118-v4-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
name: Build
command: |
VERSION=$CIRCLE_BRANCH
if [ ! -z "${CIRCLE_TAG}" ]; then VERSION=$CIRCLE_TAG; fi
make ENV_BUILD_IDENTIFIER=$VERSION ENV_BUILD_VERSION=$(echo $CIRCLE_SHA1 | cut -c1-7) build
working_directory: ~/repo118
docker:
- image: cimg/go:1.18
workflows:
build-and-release:
jobs:
#--------------- BUILD ---------------#
- build-latest:
filters:
tags:
only: /^v.*/
- build-118:
requires:
- build-latest
filters:
tags:
only: /^v.*/

View File

@@ -24,13 +24,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Get Version
shell: bash
@@ -41,18 +41,18 @@ jobs:
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v4
with:
images: wgportal/wg-portal
flavor: |
latest=true
latest=false
prefix=
suffix=
tags: |
@@ -62,7 +62,7 @@ jobs:
type=semver,pattern=v{{major}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
@@ -82,13 +82,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2
- name: Get Version
shell: bash
@@ -101,7 +101,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -111,11 +111,11 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=true
latest=false
prefix=
suffix=
tags: |
@@ -127,7 +127,7 @@ jobs:
# 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
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -1,22 +0,0 @@
name: github-pages
on:
push:
branches:
- master
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v3
with:
key: ${{ github.ref }}
path: .cache
- run: pip install mkdocs-material
- run: pip install pillow cairosvg
- run: mkdocs gh-deploy --force

8
.gitignore vendored
View File

@@ -28,14 +28,12 @@
out/
dist/
data/
docker_images/
ssh.key
.testCoverage.txt
wg_portal.db
sqlite.db
go.sum
swagger.json
swagger.yaml
/config.yml
/config/
venv/
.cache/
sqlite.db
node_modules/

View File

@@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="swag_build_tool" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="wg-portal" />
<working_directory value="$PROJECT_DIR$" />
<kind value="PACKAGE" />
<package value="github.com/h44z/wg-portal/cmd/api_build_tool" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/internal/ports/api/build_tool/main.go" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,17 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="wg-portal-migrate" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="wg-portal" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="-migrateFrom=wg_portal.db" />
<envs>
<env name="SESSION_SECRET" value="extremlybad" />
<env name="LOG_LEVEL" value="trace" />
</envs>
<sudo value="true" />
<kind value="PACKAGE" />
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
<method v="2" />
</configuration>
</component>

View File

@@ -1,16 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="wg-portal" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="wg-portal" />
<working_directory value="$PROJECT_DIR$" />
<envs>
<env name="SESSION_SECRET" value="extremlybad" />
<env name="LOG_LEVEL" value="trace" />
</envs>
<sudo value="true" />
<kind value="PACKAGE" />
<package value="github.com/h44z/wg-portal/cmd/wg-portal" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/cmd/wg-portal/main.go" />
<method v="2" />
</configuration>
</component>

View File

@@ -4,7 +4,7 @@
######-
# Start from the latest golang base image as builder image (only used to compile the code)
######-
FROM golang:1.21 as builder
FROM golang:1.18 as builder
ARG BUILD_IDENTIFIER
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
@@ -30,25 +30,24 @@ RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platfo
######-
# Here starts the main image
######-
FROM alpine:3.19
# Install OS-level dependencies
RUN apk add --no-cache bash openresolv
FROM scratch
# 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
# Copy binaries
COPY --from=builder /build/dist/wg-portal /app/wg-portal
COPY --from=builder /build/dist/hc /app/hc
# Set the Current Working Directory inside the container
WORKDIR /app
# by default, the web-portal is reachable on port 8888
EXPOSE 8888/tcp
# 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" ]
CMD [ "/app/wg-portal" ]
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]

View File

@@ -1,4 +1,4 @@
Copyright (c) 2020-2023 Christoph Haas
Copyright (c) 2020 Christoph Haas
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -5,7 +5,6 @@ GOFILES:=$(shell go list ./... | grep -v /vendor/)
BUILDDIR=dist
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
IMAGE=h44z/wg-portal
NPMCMD=npm
all: help
@@ -26,6 +25,7 @@ help:
#> codegen: Re-generate autogenerated files (like API docs)
.PHONY: codegen
codegen: $(SUBDIRS)
$(GOCMD) install github.com/swaggo/swag/cmd/swag@v1.8.10
cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/
$(GOCMD) fmt internal/server/docs/docs.go
@@ -75,56 +75,55 @@ clean:
#< build: Build all executables (architecture depends on build system)
.PHONY: build
build: build-dependencies
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-amd64: Build all executables for AMD64
.PHONY: build-amd64
build-amd64: build-dependencies
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
CGO_ENABLED=1 $(GOCMD) build -o $(BUILDDIR)/wg-portal-amd64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 $(GOCMD) build -o $(BUILDDIR)/hc-amd64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm64: Build all executables for ARM64
.PHONY: build-arm64
build-arm64: build-dependencies
CGO_ENABLED=0 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm64 \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GOCMD) build -o $(BUILDDIR)/hc-arm64 \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-arm: Build all executables for ARM32
.PHONY: build-arm
build-arm: build-dependencies
CGO_ENABLED=0 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/wg-portal-arm \
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
-tags netgo \
cmd/wg-portal/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 $(GOCMD) build -o $(BUILDDIR)/hc-arm \
-ldflags "-w -s -extldflags \"-static\"" \
cmd/hc/main.go
#< build-dependencies: Generate the output directory for compiled executables and download dependencies
.PHONY: build-dependencies
build-dependencies:
@$(GOCMD) mod download -x
@mkdir -p $(BUILDDIR)
cp scripts/wg-portal.service $(BUILDDIR)
#< frontend: Build Vue.js frontend
frontend: frontend-dependencies
cd frontend; $(NPMCMD) run build
#< frontend-dependencies: Generate the output directory for compiled executables and download frontend dependencies
.PHONY: frontend-dependencies
frontend-dependencies:
@mkdir -p $(BUILDDIR)
cd frontend; $(NPMCMD) install
#< build-docker: Build a docker image on the current host system
.PHONY: build-docker
build-docker:
docker build --progress=plain \
--build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \
--build-arg TARGETPLATFORM=unknown . \
-t h44z/wg-portal:local
cp scripts/wg-portal.env $(BUILDDIR)

51
README-RASPBERRYPI.md Normal file
View File

@@ -0,0 +1,51 @@
# WireGuard Portal on Raspberry Pi
This readme only contains a detailed explanation of how to set up the WireGuard Portal service on a raspberry pi (>= 3).
## Setup
You can either download prebuild binaries from the [release page](https://github.com/h44z/wg-portal/releases) or use Docker images for ARM.
If you want to build the binary yourself, use the following building instructions.
### Building
This section describes how to build the WireGuard Portal code.
To compile the final binary, use the Makefile provided in the repository.
As WireGuard Portal is written in Go, **golang >= 1.18** must be installed prior to building.
If you want to cross compile ARM binaries from AMD64 systems, install *arm-linux-gnueabi-gcc* (armv7) or *aarch64-linux-gnu-gcc* (arm64).
```
# for 64 bit OS
make build-arm64
# for 32 bit OS
make build-arm
```
The compiled binary and all necessary assets will be located in the dist folder.
### Service setup
- Copy the contents from the dist folder (or from the downloaded zip file) to `/opt/wg-portal`. You can choose a different path as well, but make sure to update the systemd service file accordingly.
- Update the provided systemd `wg-portal.service` file:
- Make sure that the binary matches the system architecture.
- There are three pre-build binaries available: wg-portal-**amd64**, wg-portal-**arm64** and wg-portal-**arm**.
- For a raspberry pi use the arm binary if you are using armv7l architecture. If armv8 is used, the arm64 version should work.
- Make sure that the paths to the binary and the working directory are set correctly (defaults to /opt/wg-portal/wg-portal-amd64):
- ConditionPathExists
- WorkingDirectory
- ExecStart
- EnvironmentFile
- Update environment variables in the `wg-portal.env` file to fit your needs
- Make sure that the binary application file is executable
- `sudo chmod +x /opt/wg-portal/wg-portal-*`
- Link the system service file to the correct folder:
- `sudo ln -s /opt/wg-portal/wg-portal.service /etc/systemd/system/wg-portal.service`
- Reload the systemctl daemon:
- `sudo systemctl daemon-reload`
### Manage the service
Once the service has been setup, you can simply manage the service using `systemctl`:
- Enable on startup: `systemctl enable wg-portal.service`
- Start: `systemctl start wg-portal.service`
- Stop: `systemctl stop wg-portal.service`
- Status: `systemctl status wg-portal.service`

378
README.md
View File

@@ -1,4 +1,4 @@
# WireGuard Portal (v2 - testing)
# WireGuard Portal (v1)
[![Build Status](https://travis-ci.com/h44z/wg-portal.svg?token=q4pSqaqT58Jzpxdx62xk&branch=master)](https://travis-ci.com/h44z/wg-portal)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://opensource.org/licenses/MIT)
@@ -6,203 +6,255 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/h44z/wg-portal)](https://goreportcard.com/report/github.com/h44z/wg-portal)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/h44z/wg-portal)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/h44z/wg-portal)
[![Docker Pulls](https://img.shields.io/docker/pulls/h44z/wg-portal.svg)](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**.
[![Docker Pulls](https://img.shields.io/docker/pulls/wgportal/wg-portal.svg)](https://hub.docker.com/r/h44z/wg-portal/)
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
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
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 currently supports using SQLite and MySQL as a user source for authentication and profile data.
It also supports LDAP (Active Directory or OpenLDAP) as authentication provider.
## Features
* Self-hosted - the whole application is a single binary
* Responsive web UI written in Vue.JS
* Self-hosted and web based
* Automatically select IP from the network pool assigned to client
* QR-Code for convenient mobile client configuration
* Sent email to client with QR-code and client config
* Enable / Disable clients seamlessly
* Generation of wg-quick configuration file (`wgX.conf`) if required
* User authentication (database, OAuth or LDAP)
* Generation of `wgX.conf` after any modification
* IPv6 ready
* Docker ready
* User authentication (SQLite/MySQL and LDAP)
* Dockerized
* Responsive template
* One single binary
* Can be used with existing WireGuard setups
* Support for multiple WireGuard interfaces
* REST API for management and client deployment
* Peer Expiry Feature
* Handle route and DNS settings like wg-quick does
* ~~REST API for management and client deployment~~ (coming soon)
![Screenshot](screenshot.png)
## Setup
Make sure that your host system has at least one WireGuard interface (for example wg0) available.
If you did not start up a WireGuard interface yet, take a look at [wg-quick](https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html) in order to get started.
### Docker
The easiest way to run WireGuard Portal is to use the [Docker image](https://hub.docker.com/r/wgportal/wg-portal) provided.
Since the project was accepted by the Docker-Sponsored Open Source Program, the image is now available on Docker Hub:
https://hub.docker.com/r/wgportal/wg-portal
> :warning: **HINT**: the *latest* tag always refers to the master branch and might contain unstable or incompatible code!
> For production use a fixed version or use the *stable* tag.
Docker Compose snippet with some sample configuration values:
```
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:v1
container_name: wg-portal
restart: unless-stopped
cap_add:
- NET_ADMIN
network_mode: "host"
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
ports:
- '8123:8123'
environment:
# WireGuard Settings
- WG_DEVICES=wg0
- WG_DEFAULT_DEVICE=wg0
- WG_CONFIG_PATH=/etc/wireguard
# Core Settings
- EXTERNAL_URL=https://vpn.company.com
- WEBSITE_TITLE=WireGuard VPN
- COMPANY_NAME=Your Company Name
- ADMIN_USER=admin@domain.com
- ADMIN_PASS=supersecret
# Mail Settings
- MAIL_FROM=WireGuard VPN <noreply+wireguard@company.com>
- EMAIL_HOST=10.10.10.10
- EMAIL_PORT=25
# LDAP Settings
- LDAP_ENABLED=true
- LDAP_URL=ldap://srv-ad01.company.local:389
- LDAP_BASEDN=DC=COMPANY,DC=LOCAL
- LDAP_USER=ldap_wireguard@company.local
- LDAP_PASSWORD=supersecretldappassword
- LDAP_ADMIN_GROUP=CN=WireGuardAdmins,OU=Users,DC=COMPANY,DC=LOCAL
```
Please note that mapping ```/etc/wireguard``` to ```/etc/wireguard``` inside the docker, will erase your host's current configuration.
If needed, please make sure to back up your files from ```/etc/wireguard```.
For a full list of configuration options take a look at the source file [internal/server/configuration.go](internal/server/configuration.go#L58).
### Standalone
For a standalone application, use the Makefile provided in the repository to build the application. Go version 1.18 or higher has to be installed to build WireGuard Portal.
```shell
# show all possible make commands
make
# build wg-portal for current system architecture
make build
```
The compiled binary will be located in the dist folder.
A detailed description for using this software with a raspberry pi can be found in the [README-RASPBERRYPI.md](README-RASPBERRYPI.md).
To build the Docker image, Docker (> 20.x) with buildx is required. If you want to build cross-platform images, you need to install qemu.
On arch linux for example install: `docker-buildx qemu-user-static qemu-user-static-binfmt`.
Once the Docker setup is completed, create a new buildx builder:
```shell
docker buildx create --name wgportalbuilder --platform linux/arm/v7,linux/arm64,linux/amd64
docker buildx use wgportalbuilder
docker buildx inspect --bootstrap
```
Now you can compile the Docker image:
```shell
# multi platform build, can only be exported to tar archives
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 --output type=local,dest=docker_images \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
# image for current platform only (same as docker build)
docker buildx build --load \
--build-arg BUILD_IDENTIFIER=dev --build-arg BUILD_VERSION=0.1 -t h44z/wg-portal .
```
## Configuration
You can configure WireGuard Portal using 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`.
By default, WireGuard Portal uses a SQLite database. The database is stored in **data/sqlite.db** in the working directory of the executable.
You can configure WireGuard Portal using either environment variables or a yaml configuration file.
The filepath of the yaml configuration file defaults to **config.yml** in the working directory of the executable.
It is possible to override the configuration filepath using the environment variable **CONFIG_FILE**.
For example: `CONFIG_FILE=/home/test/config.yml ./wg-portal-amd64`.
### Configuration Options
The following configuration options are available:
| 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. |
| environment | yaml | yaml_parent | default_value | description |
|----------------------------|-------------------------|-------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| LISTENING_ADDRESS | listeningAddress | core | :8123 | The address on which the web server is listening. Optional IP address and port, e.g.: 127.0.0.1:8080. |
| EXTERNAL_URL | externalUrl | core | http://localhost:8123 | The external URL where the web server is reachable. This link is used in emails that are created by the WireGuard Portal. |
| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
| CREATE_DEFAULT_PEER | createDefaultPeer | core | false | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
| DATABASE_HOST | host | database | | The mysql server address. |
| DATABASE_PORT | port | database | | The mysql server port. |
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
| DATABASE_USERNAME | user | database | | The mysql user. |
| DATABASE_PASSWORD | password | database | | The mysql password. |
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
| EMAIL_PORT | port | email | 25 | The email server port. |
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
| LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. |
| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. Users matching this filter will be synchronized with the WireGuard Portal database. |
| LDAP_SYNC_GROUP_FILTER | syncGroupFilter | ldap | | The filter string for the LDAP groups, for example: (objectClass=group). The groups are used to recursively check for admin group member ship of users. |
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
| LOG_JSON | | | false | Format log output as JSON. |
| LOG_COLOR | | | true | Colorize log output. |
| CONFIG_FILE | | | config.yml | The config file path. |
## 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
### Sample yaml configuration
config.yml:
```yaml
core:
listeningAddress: :8123
externalUrl: https://wg-test.test.com
adminUser: test@test.com
adminPass: test
editableKeys: true
createDefaultPeer: false
ldapEnabled: true
mailFrom: WireGuard VPN <noreply@test.com>
ldap:
url: ldap://10.10.10.10:389
dn: DC=test,DC=test
startTLS: false
user: wireguard@test.test
pass: test
adminGroup: CN=WireGuardAdmins,CN=Users,DC=test,DC=test
database:
typ: sqlite
database: data/wg_portal.db
email:
host: smtp.gmail.com
port: 587
tls: true
user: test@gmail.com
pass: topsecret
wg:
devices:
- wg0
- wg1
defaultDevice: wg0
configDirectory: /etc/wireguard
manageIPAddresses: true
```
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
For example:
### RESTful API
WireGuard Portal offers a RESTful API to interact with.
The API is documented using OpenAPI 2.0, the Swagger UI can be found
under the URL `http://<your wg-portal ip/domain>/swagger/index.html?displayOperationId=true`.
```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
```
The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
## 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.
* Creating or removing WireGuard (wgX) interfaces.
* Generation or application of any `iptables` or `nftables` rules.
* Setting up or changing IP-addresses of the WireGuard interface on operating systems other than linux.
* Importing private keys of an existing WireGuard setup.
## 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
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
* [go-template, data-driven templates for generating textual output](https://golang.org/pkg/text/template/)
* [Bootstrap, for the HTML templates](https://getbootstrap.com/)
* [JQuery, for some nice JavaScript effects ;)](https://jquery.com/)
## License
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).

190
assets/css/_bootswatch.scss Normal file
View File

@@ -0,0 +1,190 @@
// Lux 4.5.3
// Bootswatch
// Variables ===================================================================
$web-font-path: "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap" !default;
@import url($web-font-path);
// Navbar ======================================================================
.navbar {
font-size: $font-size-sm;
text-transform: uppercase;
font-weight: 600;
&-nav {
.nav-link {
padding-top: .715rem;
padding-bottom: .715rem;
}
}
&-brand {
margin-right: 2rem;
}
}
.bg-primary {
background-color: theme-color("primary") !important;
}
.bg-light {
border: 1px solid rgba(0, 0, 0, .1);
&.navbar-fixed-top {
border-width: 0 0 1px;
}
&.navbar-bottom-top {
border-width: 1px 0 0;
}
}
.nav-item {
margin-right: 2rem;
}
// Buttons =====================================================================
.btn {
font-size: $font-size-sm;
text-transform: uppercase;
&-sm {
font-size: 10px;
}
&-warning {
&,
&:hover,
&:not([disabled]):not(.disabled):active,
&:focus {
color: $white;
}
}
}
.btn-outline-secondary {
border-color: $gray-600;
color: $gray-600;
&:not([disabled]):not(.disabled):hover,
&:not([disabled]):not(.disabled):focus,
&:not([disabled]):not(.disabled):active {
background-color: $gray-400;
border-color: $gray-400;
color: $white;
}
&:not([disabled]):not(.disabled):focus {
box-shadow: 0 0 0 .2rem rgba($gray-400, .5);
}
}
[class*="btn-outline-"] {
border-width: 2px;
}
.border-secondary {
border: 1px solid $gray-400 !important;
}
// Typography ==================================================================
body {
font-weight: 200;
letter-spacing: 1px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-transform: uppercase;
letter-spacing: 3px;
}
.text-secondary {
color: $body-color !important;
}
// Tables ======================================================================
th {
font-size: $font-size-sm;
text-transform: uppercase;
}
.table {
th,
td {
padding: 1.5rem;
}
&-sm {
th,
td {
padding: .75rem;
}
}
}
// Forms =======================================================================
.custom-switch {
.custom-control-label {
&::after {
top: add(.15625rem, 2px);
left: add(-2.25rem, 2px);
width: subtract(1rem, 4px);
height: subtract(1rem, 4px);
}
}
}
// Navs ========================================================================
.dropdown-menu {
font-size: $font-size-sm;
text-transform: none;
}
// Indicators ==================================================================
.badge {
padding-top: .28rem;
&-pill {
border-radius: 10rem;
}
}
// Containers ==================================================================
.list-group-item {
h1,
h2,
h3,
h4,
h5,
h6,
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
color: inherit;
}
}
.card {
&-title,
&-header {
color: inherit;
}
}

106
assets/css/_variables.scss Normal file
View File

@@ -0,0 +1,106 @@
// Lux 4.5.3
// Bootswatch
//
// Color system
//
$white: #fff !default;
$gray-100: #f8f9fa !default;
$gray-200: #f7f7f9 !default;
$gray-300: #eceeef !default;
$gray-400: #ced4da !default;
$gray-500: #adb5bd !default;
$gray-600: #919aa1 !default;
$gray-700: #55595c !default;
$gray-800: #343a40 !default;
$gray-900: #1a1a1a !default;
$black: #000 !default;
$blue: #007bff !default;
$indigo: #6610f2 !default;
$purple: #6f42c1 !default;
$pink: #e83e8c !default;
$red: #d9534f !default;
$orange: #fd7e14 !default;
$yellow: #f0ad4e !default;
$green: #4bbf73 !default;
$teal: #20c997 !default;
$cyan: #1f9bcf !default;
$primary: $gray-900 !default;
$secondary: $white !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $white !default;
$dark: $gray-800 !default;
$yiq-contrasted-threshold: 185 !default;
// Options
$enable-rounded: false !default;
// Body
$body-color: $gray-700 !default;
// Fonts
// stylelint-disable-next-line value-keyword-case
$font-family-sans-serif: "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default;
$font-size-base: .875rem !default;
$h1-font-size: 2rem !default;
$h2-font-size: 1.75rem !default;
$h3-font-size: 1.5rem !default;
$h4-font-size: 1.25rem !default;
$h5-font-size: 1rem !default;
$h6-font-size: .75rem !default;
$headings-font-weight: 600 !default;
$headings-color: $gray-900 !default;
// Tables
$table-border-color: rgba(0, 0, 0, .05) !default;
// Buttons + Forms
$input-btn-border-width: 0 !default;
// Buttons
$btn-line-height: 1.5rem !default;
$input-btn-padding-y: .75rem !default;
$input-btn-padding-x: 1.5rem !default;
$input-btn-padding-y-sm: .5rem !default;
$input-btn-padding-x-sm: 1rem !default;
$input-btn-padding-y-lg: 2rem !default;
$input-btn-padding-x-lg: 2rem !default;
$btn-font-weight: 600 !default;
// Forms
$input-line-height: 1.5 !default;
$input-bg: $gray-200 !default;
$input-disabled-bg: $gray-300 !default;
$input-group-addon-bg: $gray-300 !default;
// Navbar
$navbar-padding-y: 1.5rem !default;
$navbar-dark-hover-color: $white !default;
$navbar-light-color: rgba($black, .3) !default;
$navbar-light-hover-color: $gray-900 !default;
$navbar-light-active-color: $gray-900 !default;
// Pagination
$pagination-border-color: transparent !default;
$pagination-hover-border-color: $pagination-border-color !default;
$pagination-disabled-border-color: $pagination-border-color !default;
// Breadcrumbs
$breadcrumb-bg: transparent !default;

View File

@@ -0,0 +1,5 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/@-webkit-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes blink{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{height:auto;min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px}

118
assets/css/custom.css Normal file
View File

@@ -0,0 +1,118 @@
/* THEME STYLE */
pre{background:#f7f7f9}iframe{overflow:hidden;border:none}@media (min-width: 768px){body>.navbar-transparent{box-shadow:none}body>.navbar-transparent .navbar-nav>.open>a{box-shadow:none}}#home,#help{font-size:.9rem}#home .navbar,#help .navbar{background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);transition:box-shadow 200ms ease-in}#home .navbar-transparent,#help .navbar-transparent{background:none !important;box-shadow:none}#home .navbar-brand .nav-link,#help .navbar-brand .nav-link{display:inline-block;margin-right:-30px}#home .nav-link,#help .nav-link{text-transform:uppercase;font-weight:500;color:#fff}#home{padding-top:0}#home .btn{padding:.6rem .55rem .5rem;box-shadow:none;font-size:.7rem;font-weight:500}.bs-docs-section{margin-top:4em}.bs-docs-section .page-header h1{padding:2rem 0;font-size:3rem}.dropdown-menu.show[aria-labelledby="themes"]{display:-ms-flexbox;display:flex;width:420px;-ms-flex-wrap:wrap;flex-wrap:wrap}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item{width:33.333%}.dropdown-menu.show[aria-labelledby="themes"] .dropdown-item:first-child{width:100%}.bs-component{position:relative}.bs-component+.bs-component{margin-top:1rem}.bs-component .card{margin-bottom:1rem}.bs-component .modal{position:relative;top:auto;right:auto;left:auto;bottom:auto;z-index:1;display:block}.bs-component .modal-dialog{width:90%}.bs-component .popover{position:relative;display:inline-block;width:220px;margin:20px}.source-button{display:none;position:absolute;top:0;right:0;z-index:100;font-weight:700}.source-button:hover{cursor:pointer}.bs-component:hover .source-button{display:block}#source-modal pre{max-height:calc(100vh - 11rem);background-color:rgba(0,0,0,0.7);color:rgba(255,255,255,0.7)}.nav-tabs{margin-bottom:15px}.progress{margin-bottom:10px}#footer{margin:5em 0}#footer li{float:left;margin-right:1.5em;margin-bottom:1.5em}#footer p{clear:left;margin-bottom:0}.splash{padding:12em 0 6em;background:#349aed;background:linear-gradient(145deg, #349aed 50%, #34d8ed 100%);color:#fff;text-align:center}.splash .logo{width:160px}.splash h1{font-size:3em;color:#fff}.splash #social{margin:2em 0 3em}.splash .alert{margin:2em 0;border:none}.splash .sponsor a{color:#fff}.section-tout{padding:6em 0 1em;border-bottom:1px solid rgba(0,0,0,0.05);background-color:#eaf1f1;text-align:center}.section-tout .icon{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;width:80px;height:80px;margin:0 auto 1rem;background:#349aed;background:linear-gradient(145deg, #3b9cea 50%, #3db8eb 100%);border-radius:50%;font-size:2rem;color:rgba(0,0,0,0.5)}.section-tout p{margin-bottom:5em}.section-preview{padding:4em 0}.section-preview .preview{margin-bottom:4em;background-color:#eaf1f1}.section-preview .preview .image{position:relative}.section-preview .preview .image::before{box-shadow:inset 0 0 0 1px rgba(0,0,0,0.1);position:absolute;top:0;left:0;width:100%;height:100%;content:"";pointer-events:none}.section-preview .preview .options{padding:2em;border:1px solid rgba(0,0,0,0.05);border-top:none;text-align:center}.section-preview .preview .options p{margin-bottom:2em}.section-preview .dropdown-menu{text-align:left}.section-preview .lead{margin-bottom:2em}.sponsor #carbonads{max-width:240px;margin:0 auto}.sponsor .carbon-text{display:block;margin-top:1em;font-size:12px}.sponsor .carbon-poweredby{float:right;margin-top:1em;font-size:10px}@media (max-width: 767px){.splash{padding-top:8em}.splash .logo{width:100px}.splash h1{font-size:2em}#banner{margin-bottom:2em;text-align:center}}
/* CUSTOM STYLE */
/* Start collapsable table
-------------------------------------------------- */
.hiddenRow, .hiddenCell {
padding: 0px!important;
border-top: 0px!important;
}
.collapsedRow .col-md-6{
display:inline-block;
}
.collapsedRow {
padding: 10px 0px;
border-top: 1px solid lightgray;
margin-left: 0px;
margin-right: 0px;
}
.collapse-indicator {
text-decoration: none;
}
.collapse-indicator:after {
font-weight: 900;
font-family: "Font Awesome 5 Free";
content: "\f056";
}
.collapse-indicator.collapsed:after {
font-weight: 900;
font-family: "Font Awesome 5 Free";
content: "\f055";
}
/* --------------------------------------------------
End collapsable table*/
.jumbotron-home {
padding: 1rem 1rem;
}
@media (min-width: 576px) {
.jumbotron-home {
padding: 2rem 2rem;
}
}
@media (min-width: 1440px) {
.container, .container-lg, .container-md, .container-sm, .container-xl {
max-width: 1400px;
}
}
.device-status-table {
font-size: small;
}
.navbar {
padding: 0.5rem 1rem;
}
.navbar-brand > img {
height: 2rem;
width: auto;
}
.disabled-peer {
color: #d03131;
}
.expiring-peer {
color: #d09d12;
}
.tokenfield .token {
border-radius: 0px;
border: 1px solid #1a1a1a;
color: #1a1a1a;
background-color: #f7f7f9;
margin: -4px 5px 5px 0;
height: 22px;
}
.form-group.required label:after {
content:"*";
color:red;
}
a.advanced-settings:before {
content: "Hide";
}
a.advanced-settings.collapsed:before {
content: "Show";
}
.form-group.global-config label:after, .custom-control.global-config label:after {
content: "g";
color: #0057bb;
font-size: xx-small;
top: -5px;
position: absolute;
}
.text-blue {
color: #0057bb;
}
@media (min-width: 992px) {
.pull-right-lg {
float: right;
}
}

7
assets/css/jquery-ui.min.css vendored Normal file

File diff suppressed because one or more lines are too long

8
assets/css/signin.css Normal file
View File

@@ -0,0 +1,8 @@
.navbar {
padding: 0.5rem 1rem;
}
.navbar-brand > img {
height: 2rem;
width: auto;
}

View File

@@ -0,0 +1,5 @@
/*!
* bootstrap-tokenfield
* https://github.com/sliptree/bootstrap-tokenfield
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
*/.twitter-typeahead{width:100%;position:relative;vertical-align:top}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{margin:0;width:100%;vertical-align:middle;background-color:#fff}.twitter-typeahead .tt-hint{color:#999;z-index:1;border:1px solid transparent}.twitter-typeahead .tt-input{color:#555;z-index:2}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429}.twitter-typeahead .input-sm.tt-input,.twitter-typeahead .hint-sm.tt-hint{border-radius:3px}.twitter-typeahead .input-lg.tt-input,.twitter-typeahead .hint-lg.tt-hint{border-radius:6px}.input-group .twitter-typeahead:first-child .tt-input,.input-group .twitter-typeahead:first-child .tt-hint{border-radius:4px 0 0 4px!important}.input-group .twitter-typeahead:last-child .tt-input,.input-group .twitter-typeahead:last-child .tt-hint{border-radius:0 4px 4px 0!important}.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint{border-radius:3px 0 0 3px!important}.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint{border-radius:0 3px 3px 0!important}.input-sm.tt-input,.hint-sm.tt-hint,.input-group.input-group-sm .tt-input,.input-group.input-group-sm .tt-hint{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint{border-radius:6px 0 0 6px!important}.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint{border-radius:0 6px 6px 0!important}.input-lg.tt-input,.hint-lg.tt-hint,.input-group.input-group-lg .tt-input,.input-group.input-group-lg .tt-hint{height:45px;padding:10px 16px;font-size:18px;line-height:1.33}.tt-dropdown-menu{width:100%;min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);*border-right-width:2px;*border-bottom-width:2px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-cursor{color:#262626;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.tt-suggestion.tt-cursor a{color:#fff}.tt-suggestion p{margin:0}.tokenfield .twitter-typeahead{width:auto}.tokenfield .twitter-typeahead .tt-hint{padding:0;height:20px}.tokenfield.input-sm .twitter-typeahead .tt-input,.tokenfield.input-sm .twitter-typeahead .tt-hint{height:18px;font-size:12px;line-height:1.5}.tokenfield.input-lg .twitter-typeahead .tt-input,.tokenfield.input-lg .twitter-typeahead .tt-hint{height:23px;font-size:18px;line-height:1.33}.tokenfield .twitter-typeahead .tt-suggestions{font-size:14px}

View File

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 692 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

Before

Width:  |  Height:  |  Size: 829 KiB

After

Width:  |  Height:  |  Size: 829 KiB

4
assets/fonts/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

13
assets/js/bootstrap-confirmation.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
assets/js/bootstrap-tokenfield.min.js vendored Normal file

File diff suppressed because one or more lines are too long

39
assets/js/custom.js Normal file
View File

@@ -0,0 +1,39 @@
(function($) {
"use strict"; // Start of use strict
// Smooth scrolling using jQuery easing
$(document).on('click', 'a.scroll-to-top', function(e) {
var $anchor = $(this);
$('html, body').stop().animate({
scrollTop: ($($anchor.attr('href')).offset().top)
}, 1000, 'easeInOutExpo');
e.preventDefault();
});
$(function () {
$('[data-toggle="tooltip"]').tooltip()
});
$(".online-status").each(function(){
const onlineStatusID = "#" + $(this).attr('id');
$.get( "/user/status?pkey=" + encodeURIComponent($(this).attr('data-pkey')), function( data ) {
console.log(onlineStatusID + " " + data)
if(data === true) {
$(onlineStatusID).html('<i class="fas fa-link text-success"></i>');
} else {
$(onlineStatusID).html('<i class="fas fa-unlink"></i>');
}
});
});
$(function() {
$('select.device-selector').change(function() {
this.form.submit();
});
});
$('[data-toggle=confirmation]').confirmation({
rootSelector: '[data-toggle=confirmation]',
// other options
});
})(jQuery); // End of use strict

13
assets/js/jquery-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/jquery-ui.min.css">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/css/tokenfield-typeahead.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<h1>Create new clients</h1>
<h2>Enter valid user email addresses to quickly create new accounts.</h2>
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputEmail">Email Addresses</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.FormData.Emails}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputIdentifier">Client Friendly Name (will be added as suffix to the name of the user)</label>
<input type="text" name="identifier" class="form-control" id="inputIdentifier" value="{{.FormData.Identifier}}" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/bootstrap-tokenfield.min.js"></script>
<script src="/js/custom.js"></script>
<script>$('#inputEmail').on('tokenfield:createdtoken', function (e) {
// Über-simplistic e-mail validation
var re = /\S+@\S+\.\S+/
var valid = re.test(e.attrs.value)
if (!valid) {
$(e.relatedTarget).addClass('invalid')
}
}).on('tokenfield:createtoken', function (e) {
var existingTokens = $(this).tokenfield('getTokens');
$.each(existingTokens, function(index, token) {
if (token.value === e.attrs.value)
e.preventDefault();
});
}).tokenfield({
autocomplete: {
source: [{{range $i, $u :=.Users}}{{if ne $i 0}},{{end}}'{{$u.Email}}'{{end}}],
delay: 100
},
inputType: 'email',
createTokensOnBlur: true,
showAutocompleteOnFocus: false
})</script>
</body>
</html>

View File

@@ -0,0 +1,221 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
{{if eq .Device.Type "server"}}
{{if .Peer.IsNew}}
<h1>Create a new client</h1>
{{else}}
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
{{end}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="endpoint" value="{{.Peer.Endpoint}}">
{{if .EditableKeys}}
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Peer.PrivateKey}}">
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="server_PresharedKey" value="{{.Peer.PresharedKey}}">
</div>
</div>
{{else}}
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
<input type="hidden" name="presharedkey" value="{{.Peer.PresharedKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_ro_PublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Peer.PublicKey}}">
</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_Identifier">Client Friendly Name</label>
<input type="text" name="identifier" class="form-control" id="server_Identifier" value="{{.Peer.Identifier}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_Email">Client Email Address</label>
<input type="email" name="mail" class="form-control" id="server_Email" value="{{.Peer.Email}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_IP">Client IP Address</label>
<input type="text" name="ip" class="form-control" id="server_IP" value="{{.Peer.IPsStr}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 global-config">
<label for="server_AllowedIP">Allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_AllowedIPSrv">Extra Allowed IPs (Server sided)</label>
<input type="text" name="allowedipSrv" class="form-control" id="server_AllowedIPSrv" value="{{.Peer.AllowedIPsSrvStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 global-config">
<label for="server_DNS">Client DNS Servers</label>
<input type="text" name="dns" class="form-control" id="server_DNS" value="{{.Peer.DNSStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6 global-config">
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
</div>
<div class="form-group col-md-6 global-config">
<label for="server_MTU">Client MTU (0 = default)</label>
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Peer.Mtu}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="server_Disabled">
Disabled
</label>
</div>
<div class="custom-control custom-switch">
<input class="custom-control-input" name="ignoreglobalsettings" type="checkbox" value="true" id="server_IgnoreGlobalSettings" {{if .Peer.IgnoreGlobalSettings}}checked{{end}}>
<label class="custom-control-label" for="server_IgnoreGlobalSettings">
Ignore global settings (<span class="text-blue">g</span>)
</label>
</div>
</div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
{{end}}
<!-- client mode -->
{{if eq .Device.Type "client"}}
{{if .Peer.IsNew}}
<h1>Create a new remote endpoint</h1>
{{else}}
<h1>Edit remote endpoint: <strong>{{.Peer.Identifier}}</strong></h1>
{{end}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<input type="hidden" name="mail" value="{{.AdminEmail}}">
<input type="hidden" name="devicetype" value="{{.Device.Type}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="privkey" value="{{.Peer.PrivateKey}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_Identifier">Endpoint Friendly Name</label>
<input type="text" name="identifier" class="form-control" id="client_Identifier" value="{{.Peer.Identifier}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_Endpoint">Endpoint Address</label>
<input type="text" name="endpoint" class="form-control" id="client_Endpoint" value="{{.Peer.Endpoint}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PublicKey">Endpoint Public Key</label>
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Peer.PublicKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PresharedKey">Preshared Key</label>
<input type="text" name="presharedkey" class="form-control" id="client_PresharedKey" value="{{.Peer.PresharedKey}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_AllowedIP">Allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="client_AllowedIP" value="{{.Peer.AllowedIPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="client_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="client_PersistentKeepalive" placeholder="16" value="{{.Peer.PersistentKeepalive}}">
</div>
<div class="form-group col-md-6">
<label for="client_IP">Ping-Check IP Address</label>
<input type="text" name="ip" class="form-control" id="client_IP" value="{{.Peer.IPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="client_Disabled" {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="client_Disabled">
Disabled
</label>
</div>
</div>
<div class="form-group col-md-6">
<label for="expires_at">Expires At</label>
<input type="date" name="expires_at" pattern="\d{4}-\d{2}-\d{2}" class="form-control" id="expires_at" placeholder="" value="{{formatDate .Peer.ExpiresAt}}" min="2022-01-01">
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
{{end}}
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5 main-app">
<h1>Edit interface <strong>{{.Device.DeviceName}}</strong></h1>
{{template "prt_flashes.html" .}}
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {{if eq .Device.Type "server"}}active{{end}}" data-toggle="tab" href="#server">Server Mode</a>
</li>
<li class="nav-item">
<a class="nav-link {{if eq .Device.Type "client"}}active{{end}}" data-toggle="tab" href="#client">Client Mode</a>
</li>
</ul>
<div id="configContent" class="tab-content">
<!-- server mode -->
<div class="tab-pane fade {{if eq .Device.Type "server"}}active show{{end}}" id="server">
<form method="post" enctype="multipart/form-data" name="server">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="server">
<h3>Server's interface configuration</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_DisplayName">Display Name</label>
<input type="text" name="displayname" class="form-control" id="server_DisplayName" value="{{.Device.DisplayName}}">
</div>
</div>
{{if .EditableKeys}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="server_PrivateKey" value="{{.Device.PrivateKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Device.PublicKey}}" required>
</div>
</div>
{{else}}
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_ro_PublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="server_ro_PublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group required col-md-6">
<label for="server_ListenPort">Listen port</label>
<input type="number" name="port" class="form-control" id="server_ListenPort" placeholder="51820" value="{{.Device.ListenPort}}" required>
</div>
<div class="form-group required col-md-6">
<label for="server_IPs">Server IP address</label>
<input type="text" name="ip" class="form-control" id="server_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
</div>
</div>
<h3>Client's global configuration (<span class="text-blue">g</span>)</h3>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicEndpoint">Public Endpoint for Clients</label>
<input type="text" name="endpoint" class="form-control" id="server_PublicEndpoint" placeholder="vpn.company.com:51820" value="{{.Device.DefaultEndpoint}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="server_DNS">DNS Servers</label>
<input type="text" name="dns" class="form-control" id="server_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
</div>
<div class="form-group col-md-6">
<label for="server_AllowedIP">Default allowed IPs</label>
<input type="text" name="allowedip" class="form-control" id="server_AllowedIP" placeholder="10.6.6.0/24" value="{{.Device.DefaultAllowedIPsStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="server_MTU">MTU (also used for the server interface, 0 = default)</label>
<input type="number" name="mtu" class="form-control" id="server_MTU" placeholder="" value="{{.Device.Mtu}}">
</div>
<div class="form-group col-md-6">
<label for="server_PersistentKeepalive">Persistent Keepalive (0 = off)</label>
<input type="number" name="keepalive" class="form-control" id="server_PersistentKeepalive" placeholder="16" value="{{.Device.DefaultPersistentKeepalive}}">
</div>
</div>
<h3>Interface configuration hooks</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PreUp">Pre Up</label>
<input type="text" name="preup" class="form-control" id="server_PreUp" value="{{.Device.PreUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PostUp">Post Up</label>
<input type="text" name="postup" class="form-control" id="server_PostUp" value="{{.Device.PostUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PreDown">Pre Down</label>
<input type="text" name="predown" class="form-control" id="server_PreDown" value="{{.Device.PreDown}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="server_PostDown">Post Down</label>
<input type="text" name="postdown" class="form-control" id="server_PostDown" value="{{.Device.PostDown}}">
</div>
</div>
<div class="form-row">
<div class="d-flex align-items-center">
<a href="#" class="advanced-settings btn btn-link collapsed" data-toggle="collapse" data-target="#collapseAdvancedServer" aria-expanded="false" aria-controls="collapseAdvancedServer">
Advanced Settings
</a>
</div>
</div>
<div id="collapseAdvancedServer" class="collapse" aria-labelledby="collapseAdvancedServer">
<div class="form-row">
<div class="form-group col-md-6">
<label for="server_FirewallMark">Firewall Mark (0 = default or off)</label>
<input type="number" name="firewallmark" class="form-control" id="server_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
</div>
<div class="form-group col-md-6">
<label for="server_RoutingTable">Routing Table (empty = default or auto)</label>
<input type="text" name="routingtable" class="form-control" id="server_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="saveconfig" type="checkbox" value="true" id="server_SaveConfig" {{if .Peer.SaveConfig}}checked{{end}}>
<label class="custom-control-label" for="server_SaveConfig">
Save Configuration (if interface was edited via WireGuard configuration tool)
</label>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
<a href="/admin/device/applyglobals" class="btn btn-dark float-right">Apply Global Settings (<span class="text-blue">g</span>) to clients</a>
</form>
</div>
<!-- client mode -->
<div class="tab-pane fade {{if eq .Device.Type "client"}}active show{{end}}" id="client">
<form method="post" enctype="multipart/form-data" name="client">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="device" value="{{.Device.DeviceName}}">
<input type="hidden" name="devicetype" value="client">
<h3>Client's interface configuration</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_DisplayName">Display Name</label>
<input type="text" name="displayname" class="form-control" id="client_DisplayName" value="{{.Device.DisplayName}}">
</div>
</div>
{{if .EditableKeys}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PrivateKey">Private Key</label>
<input type="text" name="privkey" class="form-control" id="client_PrivateKey" value="{{.Device.PrivateKey}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="client_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="client_PublicKey" value="{{.Device.PublicKey}}" required>
</div>
</div>
{{else}}
<input type="hidden" name="privkey" value="{{.Device.PrivateKey}}">
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_ro_PublicKey">Public Key</label>
<input type="text" name="pubkey" readonly class="form-control" id="client_ro_PublicKey" value="{{.Device.PublicKey}}">
</div>
</div>
{{end}}
<div class="form-row">
<div class="form-group required col-md-6">
<label for="client_IPs">Client IP address</label>
<input type="text" name="ip" class="form-control" id="client_IPs" placeholder="10.6.6.1/24" value="{{.Device.IPsStr}}" required>
</div>
<div class="form-group col-md-6">
<label for="client_DNS">DNS Servers</label>
<input type="text" name="dns" class="form-control" id="client_DNS" placeholder="1.1.1.1" value="{{.Device.DNSStr}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="client_MTU">MTU (0 = default)</label>
<input type="number" name="mtu" class="form-control" id="client_MTU" placeholder="" value="{{.Device.Mtu}}">
</div>
<div class="form-group col-md-4">
<label for="client_FirewallMark">Firewall Mark (0 = default or off)</label>
<input type="number" name="firewallmark" class="form-control" id="client_FirewallMark" placeholder="" value="{{.Device.FirewallMark}}">
</div>
<div class="form-group col-md-4">
<label for="client_RoutingTable">Routing Table (empty = default or auto)</label>
<input type="text" name="routingtable" class="form-control" id="client_RoutingTable" placeholder="auto" value="{{.Device.RoutingTable}}">
</div>
</div>
<h3>Interface configuration hooks</h3>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PreUp">Pre Up</label>
<input type="text" name="preup" class="form-control" id="client_PreUp" value="{{.Device.PreUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PostUp">Post Up</label>
<input type="text" name="postup" class="form-control" id="client_PostUp" value="{{.Device.PostUp}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PreDown">Pre Down</label>
<input type="text" name="predown" class="form-control" id="client_PreDown" value="{{.Device.PreDown}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="client_PostDown">Post Down</label>
<input type="text" name="postdown" class="form-control" id="client_PostDown" value="{{.Device.PostDown}}">
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Users</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{if eq .User.CreatedAt .Epoch}}
<h1>Create a new user</h1>
{{else}}
<h1>Edit user <strong>{{.User.Email}}</strong></h1>
{{end}}
{{template "prt_flashes.html" .}}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
{{if eq .User.CreatedAt .Epoch}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputEmail">Email</label>
<input type="text" name="email" class="form-control" id="inputEmail" value="{{.User.Email}}" required>
</div>
</div>
{{else}}
<input type="hidden" name="email" value="{{.User.Email}}">
{{end}}
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputFirstname">Firstname</label>
<input type="text" name="firstname" class="form-control" id="inputFirstname" value="{{.User.Firstname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group required col-md-12">
<label for="inputLastname">Lastname</label>
<input type="text" name="lastname" class="form-control" id="inputLastname" value="{{.User.Lastname}}" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<label for="inputPhone">Phone</label>
<input type="text" name="phone" class="form-control" id="inputPhone" value="{{.User.Phone}}">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12 {{if eq .User.CreatedAt .Epoch}}required{{end}}">
<label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword" {{if eq .User.CreatedAt .Epoch}}required{{end}}>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isadmin" type="checkbox" value="true" id="inputAdmin" {{if .User.IsAdmin}}checked{{end}}>
<label class="custom-control-label" for="inputAdmin">
Administrator
</label>
</div>
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="inputDisabled" {{if .User.DeletedAt.Valid}}checked{{end}}>
<label class="custom-control-label" for="inputDisabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/admin/users/" class="btn btn-secondary">Cancel</a>
{{if eq $.Session.IsAdmin true}}
{{if eq .User.Source "db"}}
<a href="/admin/users/delete?pkey={{.User.Email}}" data-toggle="confirmation" data-title="Really delete user and associated peers?" title="Delete user and associated peers" class="btn btn-danger float-right">Delete</a>
{{end}}
{{end}}
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

286
assets/tpl/admin_index.html Normal file
View File

@@ -0,0 +1,286 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<h1>WireGuard VPN Administration</h1>
{{template "prt_flashes.html" .}}
<div class="card">
<div class="card-header">
<div class="d-flex align-items-center">
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
&nbsp;&nbsp;&nbsp;
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
&nbsp;&nbsp;&nbsp;
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
</div>
</div>
<div class="card-body">
<div class="row">
{{if eq $.Device.Type "server"}}
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>Public Key:</td>
<td>{{.Device.PublicKey}}</td>
</tr>
<tr>
<td>Public Endpoint:</td>
<td>{{.Device.DefaultEndpoint}}</td>
</tr>
<tr>
<td>Listening Port:</td>
<td>{{.Device.ListenPort}}</td>
</tr>
<tr>
<td>Enabled Peers:</td>
<td>{{len .Device.Interface.Peers}}</td>
</tr>
<tr>
<td>Total Peers:</td>
<td>{{.TotalPeers}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>IP Address:</td>
<td>{{.Device.IPsStr}}</td>
</tr>
<tr>
<td>Default allowed IP's:</td>
<td>{{.Device.DefaultAllowedIPsStr}}</td>
</tr>
<tr>
<td>Default DNS servers:</td>
<td>{{.Device.DNSStr}}</td>
</tr>
<tr>
<td>Default MTU:</td>
<td>{{.Device.Mtu}}</td>
</tr>
<tr>
<td>Default Keepalive Interval:</td>
<td>{{.Device.DefaultPersistentKeepalive}}</td>
</tr>
</tbody>
</table>
</div>
{{end}}
{{if eq $.Device.Type "client"}}
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>Public Key:</td>
<td>{{.Device.PublicKey}}</td>
</tr>
<tr>
<td>Enabled Endpoints:</td>
<td>{{len .Device.Interface.Peers}}</td>
</tr>
<tr>
<td>Total Endpoints:</td>
<td>{{.TotalPeers}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm-6">
<table class="table table-sm table-borderless device-status-table">
<tbody>
<tr>
<td>IP Address:</td>
<td>{{.Device.IPsStr}}</td>
</tr>
<tr>
<td>DNS servers:</td>
<td>{{.Device.DNSStr}}</td>
</tr>
<tr>
<td>Default MTU:</td>
<td>{{.Device.Mtu}}</td>
</tr>
</tbody>
</table>
</div>
{{end}}
</div>
</div>
</div>
<div class="mt-4 row">
<div class="col-sm-8 col-12">
{{if eq $.Device.Type "server"}}
<h2 class="mt-2">Current VPN Peers</h2>
{{end}}
{{if eq $.Device.Type "client"}}
<h2 class="mt-2">Current VPN Endpoints</h2>
{{end}}
</div>
<div class="col-sm-4 col-12 text-right">
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
{{if eq $.Device.Type "server"}}
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
{{end}}
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
<tr>
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
{{if eq $.Device.Type "server"}}
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
{{end}}
{{if eq $.Device.Type "server"}}
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
{{end}}
{{if eq $.Device.Type "client"}}
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
{{end}}
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
<th scope="col"></th><!-- Actions -->
</tr>
</thead>
<tbody>
{{range $i, $p :=.Peers}}
{{$peerUser:=(userForEmail $.Users $p.Email)}}
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
<th scope="row" class="list-image-cell">
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
<!-- online check -->
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th>
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td>
{{if eq $.Device.Type "server"}}
<td>{{$p.Email}}</td>
{{end}}
{{if eq $.Device.Type "server"}}
<td>{{$p.IPsStr}}</td>
{{end}}
{{if eq $.Device.Type "client"}}
<td>{{$p.Endpoint}}</td>
{{end}}
<td>
<span
data-toggle="tooltip"
data-placement="left"
title=""
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
{{$p.LastHandshakeRelativeTime}}
</span>
</td>
<td>
{{if eq $.Session.IsAdmin true}}
<a href="/admin/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
{{end}}
</td>
</tr>
<tr class="hiddenRow">
<td colspan="7" class="hiddenCell" style="white-space:nowrap">
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
<div class="row collapsedRow">
<div class="col-md-6 leftBorder">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
</li>
{{if eq $.Device.Type "server"}}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
</li>
{{end}}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a>
</li>
</ul>
<div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4>
{{if not $peerUser}}
<p>No user information available...</p>
{{else}}
<ul>
<li>Firstname: {{$peerUser.Firstname}}</li>
<li>Lastname: {{$peerUser.Lastname}}</li>
<li>Phone: {{$peerUser.Phone}}</li>
<li>Mail: {{$peerUser.Email}}</li>
</ul>
{{end}}
<h4>Connection / Traffic</h4>
{{if not $p.Peer}}
<p>No Traffic data available...</p>
{{else}}
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-network-wired" title="Last Endpoint"></i> {{$p.Peer.Endpoint}}{{end}}</p>
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
{{end}}
</div>
{{if eq $.Device.Type "server"}}
<div id="t2{{$p.UID}}" class="tab-pane fade">
<pre>{{$p.Config}}</pre>
</div>
{{end}}
<div id="t3{{$p.UID}}" class="tab-pane fade">
<a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a>
</div>
</div>
</div>
<div class="col-md-3">
{{if eq $.Device.Type "server"}}
<img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
{{end}}
</div>
<div class="col-md-3">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
{{if eq $.Device.Type "server"}}
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div>
{{end}}
</div>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Users</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<h1>WireGuard VPN Users</h1>
{{template "prt_flashes.html" .}}
<div class="mt-4 row">
<div class="col-sm-10 col-12">
<h2 class="mt-2">All Users</h2>
</div>
<div class="col-sm-2 col-12 text-right">
<a href="/admin/users/create" title="Add a user" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>M</a>
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
<tr>
<th scope="col"><a href="?sort=email">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "users" "email"}}"></i></a></th>
<th scope="col"><a href="?sort=lastname">Lastname <i class="fa fa-fw {{.Session.GetSortIcon "users" "lastname"}}"></i></a></th>
<th scope="col"><a href="?sort=firstname">Firstname <i class="fa fa-fw {{.Session.GetSortIcon "users" "firstname"}}"></i></a></th>
<th scope="col"><a href="?sort=source">Source <i class="fa fa-fw {{.Session.GetSortIcon "users" "source"}}"></i></a></th>
<th scope="col"><a href="?sort=admin">Is Admin <i class="fa fa-fw {{.Session.GetSortIcon "users" "admin"}}"></i></a></th>
<th scope="col"></th><!-- Actions -->
</tr>
</thead>
<tbody>
{{range $i, $u :=.Users}}
<tr id="user-pos-{{$i}}" {{if $u.DeletedAt.Valid}}class="disabled-peer"{{end}}>
<td>{{$u.Email}}</td>
<td>{{$u.Lastname}}</td>
<td>{{$u.Firstname}}</td>
<td>{{$u.Source}}</td>
<td>{{if $u.IsAdmin}}True{{else}}False{{end}}</td>
<td>
{{if eq $.Session.IsAdmin true}}
{{if eq $u.Source "db"}}
<a href="/admin/users/edit?pkey={{$u.Email}}" title="Edit user"><i class="fas fa-cog"></i></a>
{{end}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
<p>Currently listed users: <strong>{{len .Users}}</strong></p>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -100,10 +100,10 @@
<th class="column-top" width="280" style="font-size:0pt; line-height:0pt; padding:0; margin:0; font-weight:normal; vertical-align:top;">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
{{if $.User.Firstname}}
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello {{$.User.Firstname}} {{$.User.Lastname}}</td>
{{if $.User}}
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello {{$.User.Firstname}} {{$.User.Lastname}}</td>
{{else}}
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td>
<td class="h4 pb20" style="color:#000000; font-family:'Muli', Arial,sans-serif; font-size:20px; line-height:28px; text-align:left; padding-bottom:20px;">Hello</td>
{{end}}
</tr>
<tr>

33
assets/tpl/error.html Normal file
View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Error</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top">
{{template "prt_nav.html" .}}
<div class="container">
<div class="text-center mt-5">
<div class="error mx-auto" data-text="{{.Data.Code}}">
<p class="m-0">{{.Data.Code}}</p>
</div>
<p class="text-dark mb-5 lead">{{.Data.Message}}</p>
<p class="text-black-50 mb-0">{{.Data.Details}}</p><a href="/">← Back to Dashboard</a>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

89
assets/tpl/index.html Normal file
View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<!-- Theme: https://bootswatch.com/lux/ -->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }}</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-2">
<div class="page-header">
<h1>{{ .Static.WebsiteTitle }}</h1>
</div>
{{template "prt_flashes.html" .}}
<p class="lead">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography. It aims to be faster, simpler, leaner, and more useful than IPsec, while avoiding the massive headache. It intends to be considerably more performant than OpenVPN. </p>
<h3 class="mt-3">More Information</h3>
<div class="row">
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">WireGuard Installation</div>
<div class="card-body">
<h4 class="card-title">Installation</h4>
<p class="card-text">Installation instructions for client software can be found on the official WireGuard website.</p>
<a href="https://www.wireguard.com/install/" title="WireGuard Installation" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Instructions</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">About WireGuard</div>
<div class="card-body">
<h4 class="card-title">About</h4>
<p class="card-text">WireGuard® is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.</p>
<a href="https://www.wireguard.com/" title="WireGuard" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-secondary mb-4" style="min-height: 15rem;">
<div class="card-header">About WireGuard Portal</div>
<div class="card-body">
<h4 class="card-title">WireGuard Portal</h4>
<p class="card-text">WireGuard Portal is a simple, web based configuration portal for WireGuard.</p>
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">More details</a>
</div>
</div>
</div>
</div>
<div class="jumbotron jumbotron-home">
<h2 class="display-5">VPN Profiles</h2>
<p class="lead">You can access and download your personal VPN configurations via your Userprofile.</p>
<hr class="my-4">
<p>To find all your configured profiles click on the button below.</p>
<p class="lead">
<a href="/user/profile" class="btn btn-primary btn-lg" title="User-Profile">Open My Profile</a>
</p>
</div>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
<div class="jumbotron jumbotron-home">
<h2 class="display-5">Administration Area</h2>
<p class="lead">In the administration area you can manage WireGuard peers and the server interface as well as users that are allowed to log in to the WireGuard Portal.</p>
<hr class="my-4">
<p>To find all your configured profiles click on the button below.</p>
<p class="lead">
<a href="/admin/" class="btn btn-primary btn-lg" title="WireGuard Administration">Open WireGuard Administration</a>
<a href="/admin/users/" class="btn btn-primary btn-lg" title="User Administration">Open User Administration</a>
</p>
</div>
{{end}}{{end}}
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

66
assets/tpl/login.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .static.WebsiteTitle }} - Login</title>
<meta name="description" content="{{ .static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/fonts/fontawesome5-overrides.min.css">
<link rel="stylesheet" href="/css/signin.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/"><img src="{{$.static.WebsiteLogo}}" alt="{{$.static.CompanyName}}"/></a>
<div id="topNavbar" class="navbar-collapse collapse">
</div><!--/.navbar-collapse -->
</nav>
<div class="container mt-1">
<div class="card mt-5">
<div class="card-header">Please sign in</div>
<div class="card-body">
<form class="form-signin" method="post" name="login">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<div class="form-group">
<label for="inputUsername">Username</label>
<input type="text" name="username" class="form-control" id="inputUsername" aria-describedby="usernameHelp" placeholder="Enter username or email">
</div>
<div class="form-group">
<label for="inputPassword">Password</label>
<input type="password" name="password" class="form-control" id="inputPassword" placeholder="Password">
</div>
<button class="btn btn-lg btn-primary btn-block mt-5" type="submit">Sign in</button>
{{ if eq .error true }}
<div class="alert alert-danger mt-3" role="alert">
{{.message}}
</div>
{{end}}
</form>
<div class="card o-hidden border-0 my-5">
<div class="card-body p-0">
<a href="/" class="btn btn-white btn-block text-primary btn-user">Go Home</a>
</div>
</div>
</div>
</div>
{{template "prt_flashes.html" .}}
</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{{range $flash := $.Alerts}}
<div class="alert alert-{{$flash.Type}}" role="alert">
{{$flash.Message}}
</div>
{{end}}

View File

@@ -0,0 +1,5 @@
<footer class="page-footer mt-auto">
<div class="container mt-3">
<p class="text-muted">Copyright © {{ $.Static.CompanyName }} {{$.Static.Year}}, version {{$.Static.Version}} <a class="float-right scroll-to-top" href="#page-top"><i class="fas fa-angle-up"></i></a></p>
</div>
</footer>

61
assets/tpl/prt_nav.html Normal file
View File

@@ -0,0 +1,61 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topNavbar" aria-controls="topNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="/"><img src="{{$.Static.WebsiteLogo}}" alt="{{$.Static.CompanyName}}"/></a>
<div id="topNavbar" class="navbar-collapse collapse">
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-spacer"></li>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
{{with eq $.Route "/admin/"}}
<form class="form-inline my-2 my-lg-0" method="get">
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "peers"}}">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
</form>
{{end}}
{{with eq $.Route "/admin/users/"}}
<form class="form-inline my-2 my-lg-0" method="get">
<input class="form-control mr-sm-2" name="search" type="search" placeholder="Search" aria-label="Search" value="{{index $.Session.Search "users"}}">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fa fa-search"></i></button>
</form>
{{end}}
{{end}}{{end}}
</ul>
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
{{with startsWith $.Route "/admin/"}}
<form class="form-inline my-2 my-lg-0" method="get">
<div class="form-group mr-sm-2">
<select name="device" id="inputDevice" class="form-control device-selector">
{{range $d, $dn := $.DeviceNames}}
<option value="{{$d}}" {{if eq $d $.Session.DeviceName}}selected{{end}}>{{$d}} {{if and (ne $dn "") (ne $d $dn)}}({{$dn}}){{end}}</option>
{{end}}
</select>
</div>
</form>
{{end}}
{{end}}{{end}}
{{if eq $.Session.LoggedIn true}}
<div class="nav-item dropdown">
<a href="#" class="navbar-text dropdown-toggle" data-toggle="dropdown">{{$.Session.Firstname}} {{$.Session.Lastname}} <span class="caret"></span></a>
<div class="dropdown-menu">
{{with eq $.Session.LoggedIn true}}{{with eq $.Session.IsAdmin true}}
<a class="dropdown-item" href="/admin/"><i class="fas fa-cogs"></i> Administration</a>
<a class="dropdown-item" href="/admin/users/"><i class="fas fa-users-cog"></i> User Management</a>
<div class="dropdown-divider"></div>
{{end}}{{end}}
<a class="dropdown-item" href="/user/profile"><i class="fas fa-user"></i> Profile</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/auth/logout"><i class="fas fa-sign-out-alt"></i> Logout</a>
</div>
</div>
{{else}}
<a href="/auth/login" class="navbar-text"><i class="fas fa-sign-in-alt fa-sm fa-fw mr-2 text-gray-400"></i> Login</a></li>
{{end}}
</div><!--/.navbar-collapse -->
</nav>
{{if not $.Device.IsValid}}
<div class="container">
<div class="alert alert-danger">Warning: WireGuard Interface {{$.Device.DeviceName}} is not fully configured! Configurations may be incomplete and non functional!</div>
</div>
{{end}}

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
<h1>Create a new client</h1>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Admin</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
{{template "prt_flashes.html" .}}
<!-- server mode -->
<h1>Edit client: <strong>{{.Peer.Identifier}}</strong></h1>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.Csrf}}">
<input type="hidden" name="uid" value="{{.Peer.UID}}">
<div class="form-row">
<div class="form-group required col-md-12">
<label for="server_PublicKey">Public Key</label>
<input type="text" name="pubkey" class="form-control" id="server_PublicKey" value="{{.Peer.PublicKey}}" required disabled="disabled">
</div>
</div>
<div class="form-row">
<div class="form-group col-md-12">
<div class="custom-control custom-switch">
<input class="custom-control-input" name="isdisabled" type="checkbox" value="true" id="server_Disabled" {{if .Peer.DeactivatedAt}}disabled="disabled"{{end}} {{if .Peer.DeactivatedAt}}checked{{end}}>
<label class="custom-control-label" for="server_Disabled">
Disabled
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="/user/profile" class="btn btn-secondary">Cancel</a>
</form>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

145
assets/tpl/user_index.html Normal file
View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>{{ .Static.WebsiteTitle }} - Profile</title>
<meta name="description" content="{{ .Static.WebsiteTitle }}">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
<link rel="stylesheet" href="/css/custom.css">
</head>
<body id="page-top" class="d-flex flex-column min-vh-100">
{{template "prt_nav.html" .}}
<div class="container mt-5">
<h1>WireGuard VPN User-Portal</h1>
<div class="mt-4 row">
<div class="col-sm-8 col-12">
<h2 class="mt-2">Your VPN Profiles</h2>
</div>
<div class="col-sm-4 col-12 text-right">
{{if eq $.UserManagePeers true}}
<a href="/user/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
{{end}}
</div>
</div>
<div class="mt-2 table-responsive">
<table class="table table-sm" id="userTable">
<thead>
<tr>
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "id"}}"></i></a></th>
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "pubKey"}}"></i></a></th>
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
<th scope="col"><a href="?sort=device">Interface <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "device"}}"></i></a></th>
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
{{if eq $.UserManagePeers true}}
<th scope="col"></th>
{{end}}
</tr>
</thead>
<tbody>
{{range $i, $p :=.Peers}}
{{$peerUser:=(userForEmail $.Users $p.Email)}}
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
<th scope="row" class="list-image-cell">
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
<!-- online check -->
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
</th>
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
<td>{{$p.PublicKey}}</td>
<td>{{$p.Email}}</td>
<td>{{$p.IPsStr}}</td>
<td>{{$p.DeviceName}}</td>
<td>
<span
data-toggle="tooltip"
data-placement="left"
title=""
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
{{$p.LastHandshakeRelativeTime}}
</span>
</td>
{{if eq $.UserManagePeers true}}
<td>
<a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
</td>
{{end}}
</tr>
<tr class="hiddenRow">
<td colspan="6" class="hiddenCell" style="white-space:nowrap">
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
<div class="row collapsedRow">
<div class="col-md-6 leftBorder">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
</li>
</ul>
<div class="tab-content" id="tabContent{{$p.UID}}">
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
<h4>User details</h4>
{{if not $peerUser}}
<p>No user information available...</p>
{{else}}
<ul>
<li>Firstname: {{$peerUser.Firstname}}</li>
<li>Lastname: {{$peerUser.Lastname}}</li>
<li>Phone: {{$peerUser.Phone}}</li>
<li>Mail: {{$peerUser.Email}}</li>
</ul>
{{end}}
<h4>Traffic</h4>
{{if not $p.Peer}}
<p>No Traffic data available...</p>
{{else}}
<p>{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down"></i></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
{{end}}
</div>
<div id="t2{{$p.UID}}" class="tab-pane fade">
<pre>{{$p.Config}}</pre>
</div>
</div>
</div>
<div class="col-md-3">
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
</div>
<div class="col-md-3">
{{if $p.DeactivatedAt}}
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
{{end}}
{{if $p.WillExpire}}
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
{{end}}
<div class="pull-right-lg mt-lg-5 mt-md-3">
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
</div>
</div>
</div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
</div>
</div>
{{template "prt_footer.html" .}}
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.easing.js"></script>
<script src="/js/popper.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/bootstrap-confirmation.min.js"></script>
<script src="/js/custom.js"></script>
</body>
</html>

View File

@@ -1,70 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
"github.com/swaggo/swag"
"github.com/swaggo/swag/gen"
)
// this replaces the call to: swag init --propertyStrategy pascalcase --parseDependency --parseInternal --generalInfo base.go
func main() {
wd, err := os.Getwd() // should be the project root
if err != nil {
panic(err)
}
apiBasePath := filepath.Join(wd, "/internal/app/api")
apis := []string{"v0"}
hasError := false
for _, apiVersion := range apis {
apiPath := filepath.Join(apiBasePath, apiVersion, "handlers")
apiVersion = strings.TrimLeft(apiVersion, "api-")
log.Println("")
log.Println("Generate swagger docs for API", apiVersion)
log.Println("Api path:", apiPath)
err := generateApi(apiBasePath, apiPath, apiVersion)
if err != nil {
hasError = true
logrus.Errorf("failed to generate API docs for %s: %v", apiVersion, err)
}
log.Println("Generated swagger docs for API", apiVersion)
}
if hasError {
os.Exit(1)
}
}
func generateApi(basePath, apiPath, version string) error {
err := gen.New().Build(&gen.Config{
SearchDir: apiPath,
Excludes: "",
MainAPIFile: "base.go",
PropNamingStrategy: swag.PascalCase,
OutputDir: filepath.Join(basePath, "core/assets/doc"),
OutputTypes: []string{"json", "yaml"},
ParseVendor: false,
ParseDependency: true,
MarkdownFilesDir: "",
ParseInternal: true,
GeneratedTime: false,
CodeExampleFilesDir: "",
ParseDepth: 3,
InstanceName: version,
})
if err != nil {
return fmt.Errorf("swag failed: %w", err)
}
return nil
}

35
cmd/hc/main.go Normal file
View File

@@ -0,0 +1,35 @@
// source taken from https://git.prolicht.digital/golib/healthcheck/-/blob/master/cmd/hc/main.go
package main
import (
"net/http"
"os"
"time"
)
// main checks the given URL, if the response is not 200, it will return with exit code 1
// on success, exit code 0 will be returned
func main() {
os.Exit(checkWebEndpointFromArgs())
}
func checkWebEndpointFromArgs() int {
if len(os.Args) < 2 {
return 1
}
if status := checkWebEndpoint(os.Args[1]); !status {
return 1
}
return 0
}
func checkWebEndpoint(url string) bool {
client := &http.Client{
Timeout: time.Second * 2,
}
if resp, err := client.Get(url); err != nil || resp.StatusCode < 200 || resp.StatusCode > 299 {
return false
}
return true
}

View File

@@ -2,144 +2,103 @@ package main
import (
"context"
"github.com/h44z/wg-portal/internal/app/api/core"
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/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/wireguard"
"io"
"os"
"strings"
"os/signal"
"runtime"
"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/h44z/wg-portal/internal/common/healthcheck"
"github.com/h44z/wg-portal/internal/server"
"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)
_ = setupLogger(logrus.StandardLogger())
logrus.Infof("Starting WireGuard Portal V2...")
logrus.Infof("WireGuard Portal version: %s", internal.Version)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
cfg, err := config.GetConfig()
internal.AssertNoError(err)
setupLogging(cfg)
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
cfg.LogStartupValues()
// Context for clean shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
rawDb, err := adapters.NewDatabase(cfg.Database)
internal.AssertNoError(err)
// start health check service on port 11223
healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
database, err := adapters.NewSqlRepository(rawDb)
internal.AssertNoError(err)
wireGuard := adapters.NewWireGuardRepository()
wgQuick := adapters.NewWgQuickRepo()
mailer := adapters.NewSmtpMailRepo(cfg.Mail)
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
internal.AssertNoError(err)
shouldExit, err := app.HandleProgramArgs(cfg, rawDb)
switch {
case shouldExit && err == nil:
return
case shouldExit && err != nil:
logrus.Errorf("Failed to process program args: %v", err)
os.Exit(1)
case !shouldExit:
internal.AssertNoError(err)
service := server.Server{}
if err := service.Setup(ctx); err != nil {
logrus.Fatalf("setup failed: %v", err)
}
queueSize := 100
eventBus := evbus.New(queueSize)
// Attach signal handlers to context
go func() {
osCall := <-c
logrus.Tracef("received system call: %v", osCall)
cancel() // cancel the context
}()
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
internal.AssertNoError(err)
// Start main process in background
go service.Run()
authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager)
internal.AssertNoError(err)
<-ctx.Done() // Wait until the context gets canceled
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
internal.AssertNoError(err)
// Give goroutines some time to stop gracefully
logrus.Info("stopping WireGuard Portal Server...")
time.Sleep(2 * time.Second)
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard)
internal.AssertNoError(err)
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
internal.AssertNoError(err)
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)
internal.AssertNoError(err)
err = backend.Startup(ctx)
internal.AssertNoError(err)
apiFrontend := handlersV0.NewRestApi(cfg, backend)
webSrv, err := core.NewServer(cfg, apiFrontend)
internal.AssertNoError(err)
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
// wait until context gets cancelled
<-ctx.Done()
logrus.Infof("Stopping WireGuard Portal")
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
logrus.Infof("Stopped WireGuard Portal")
logrus.Infof("stopped WireGuard Portal Server...")
logrus.Exit(0)
}
func setupLogging(cfg *config.Config) {
switch strings.ToLower(cfg.Advanced.LogLevel) {
case "trace":
logrus.SetLevel(logrus.TraceLevel)
func setupLogger(logger *logrus.Logger) error {
// Check environment variables for logrus settings
level, ok := os.LookupEnv("LOG_LEVEL")
if !ok {
level = "debug" // Default logrus level
}
useJSON, ok := os.LookupEnv("LOG_JSON")
if !ok {
useJSON = "false" // Default use human readable logging
}
useColor, ok := os.LookupEnv("LOG_COLOR")
if !ok {
useColor = "true"
}
switch level {
case "off":
logger.SetOutput(io.Discard)
case "info":
logger.SetLevel(logrus.InfoLevel)
case "debug":
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)
logger.SetLevel(logrus.DebugLevel)
case "trace":
logger.SetLevel(logrus.TraceLevel)
}
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,
})
var formatter logrus.Formatter
if useJSON == "false" {
f := new(logrus.TextFormatter)
f.TimestampFormat = "2006-01-02 15:04:05"
f.FullTimestamp = true
if useColor == "true" {
f.ForceColors = true
}
formatter = f
} else {
f := new(logrus.JSONFormatter)
f.TimestampFormat = "2006-01-02 15:04:05"
formatter = f
}
logger.SetFormatter(formatter)
return nil
}

View File

@@ -1,47 +0,0 @@
advanced:
log_level: trace
core:
admin_user: test@test.de
admin_password: secret
web:
external_url: http://localhost:8888
request_logging: true
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_filter: (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
registration_enabled: true
oidc:
- id: oidc1
provider_name: google
display_name: Login with</br>Google
base_url: https://accounts.google.com
client_id: the-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true
- id: oidc2
provider_name: google2
display_name: Login with</br>Google2
base_url: https://accounts.google.com
client_id: another-client-id-1234.apps.googleusercontent.com
client_secret: A_CLIENT_SECRET
extra_scopes:
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true

View File

@@ -2,7 +2,7 @@
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:v2
image: wgportal/wg-portal:v1
container_name: wg-portal
restart: unless-stopped
logging:
@@ -15,7 +15,5 @@ services:
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
- ./config:/app/config
# restart: no
# command: ["-migrateFrom=/app/data/wg_portal.db"]
environment:
- EXTERNAL_URL=http://localhost:8123

View File

@@ -1 +0,0 @@
wgportal.org

View File

@@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,11 +0,0 @@
To build a standalone application, use the Makefile provided in the repository.
Go version **1.21** 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
```

View File

@@ -1,81 +0,0 @@
## Image Usage
The preferred way to start WireGuard Portal as Docker container is to use Docker Compose.
A sample docker-compose.yml:
```yaml
version: '3.6'
services:
wg-portal:
image: wgportal/wg-portal:v2
restart: unless-stopped
cap_add:
- NET_ADMIN
network_mode: "host"
ports:
- "8888:8888"
volumes:
- /etc/wireguard:/etc/wireguard
- ./data:/app/data
- ./config:/app/config
```
By default, the webserver is listening on port **8888**.
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
## 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).
There are three types of tags in the repository:
#### Semantic versioned tags
For example, `1.0.19`.
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
Once these tags show up in this repository, they will never change.
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. **wgportal/wg-portal:1.0.19**, instead of the latest or canary tags.
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v1` or `1.0`.
Version **1** is currently **stable**, version **2** is in **development**.
#### latest
This is the most recent build to master! It changes a lot and is very unstable.
We recommend that you don't use it except for development purposes.
#### Branch tags
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
## Configuration
You can configure WireGuard Portal using a yaml configuration file.
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
You should mount those directories as a volume:
- /app/data
- /app/config
### Configuration Options
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
A very basic example:
```yaml
core:
admin_user: test@wg-portal.local
admin_password: secret
web:
external_url: http://localhost:8888
request_logging: true
```

View File

@@ -1,25 +0,0 @@
For production deployments of WireGuard Portal, we strongly recommend using version 1.
If you want to use version 2, please be aware that it is still in beta and not feature complete.
## 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.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 the **config.yml** configuration file.
Ensure that the new database does not contain any data!

View File

@@ -1,29 +0,0 @@
**WireGuard Portal** is a simple, web based configuration portal for [WireGuard](https://wireguard.com).
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
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.
## 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)
## Quick-Start
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).

View File

@@ -1,4 +0,0 @@
---
template: layouts/home.html
title: WireGuard Portal
---

View File

@@ -1,49 +0,0 @@
/* 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;
}

View File

@@ -1,433 +0,0 @@
{% extends "main.html" %}
<!-- Render hero under tabs -->
{% block tabs %}
{{ super() }}
<!-- Additional styles for landing page -->
<style>
/* Apply box shadow on smaller screens that don't display tabs */
@media only screen and (max-width: 1220px) {
.md-header {
box-shadow: 0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);
transition: color 250ms,background-color 250ms,box-shadow 250ms;
}
}
/* Hide main content for now */
.md-content {
display: none;
}
/* Hide table of contents */
@media screen and (min-width: 60em) {
.md-sidebar--secondary {
display: none;
}
}
/* Hide navigation */
@media screen and (min-width: 76.25em) {
.md-sidebar--primary {
display: none;
}
}
/* Get started button */
.md-typeset .md-button--primary {
color: var(--md-primary-fg-color);
background-color: var(--md-primary-bg-color);
border-color: var(--md-primary-bg-color);
}
.md-typeset .md-button--primary:hover {
color: var(--md-primary-bg-color);
background-color: var(--md-primary-fg-color);
border-color: var(--md-primary-bg-color);
}
.tx-hero {
max-width: 700px;
display: flex;
padding: .4rem;
margin: 0 auto;
text-align: center;
}
.tx-hero h1 {
font-weight: 700;
font-size: 38px;
line-height: 46px;
color: rgb(38, 38, 38);
}
.tx-hero p {
color: rgb(92, 92, 92);
font-weight: 400;
font-size: 20px;
line-height: 32px;
}
.tx-hero__image {
max-width: 1000px;
min-width: 600px;
width: 100%;
height: auto;
margin: 0 auto;
display: flex;
align-items: stretch;
}
.tx-hero__image img {
width: 100%;
height: 100%;
min-width: 0;
}
/* Secondary content styles */
.secondary-section {
background: rgb(245, 245, 245) none repeat scroll 0% 0%;
border-top: 1px solid rgb(222, 222, 222);
border-bottom: 1px solid rgb(222, 222, 222)
}
@media screen and (max-width: 1012px) {
.secondary-section {
display: block;
}
}
.secondary-section .g {
position: relative;
margin-left: auto;
margin-right: auto;
padding: 0px 40px;
max-width: 1280px;
}
.secondary-section .g .section {
font-size: 18px;
font-weight: 400;
line-height: 30px;
letter-spacing: normal;
padding: 88px 0px 116px;
}
.secondary-section .g .section.follow {
padding-top: 0px;
}
.secondary-section .g .section .component-wrapper {
display: flex;
-moz-box-align: center;
align-items: center;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper {
display: block;
}
}
.secondary-section .g .section .component-wrapper h3 {
color: rgb(38, 38, 38);
font-size: 36px;
font-weight: 700;
line-height: 46px;
letter-spacing: normal;
margin-bottom: 12px;
}
.secondary-section .g .section .component-wrapper h4 {
color: rgb(38, 38, 38);
}
.secondary-section .g .section .component-wrapper p {
color: rgb(92, 92, 92);
font-size: 18px;
font-weight: 400;
line-height: 30px;
letter-spacing: normal;
margin-bottom: 16px;
}
.secondary-section .g .section .component-wrapper .image-wrapper {
margin-bottom: 12px;
overflow: hidden;
border-radius: 8px;
margin-top: 48px;
border: 1px solid rgb(222, 222, 222);
box-shadow: rgba(202, 202, 202, 0.15) 0px 0px 0px 6px;
max-width: 600px;
width: 100%;
height: auto;
margin: 0 auto;
display: flex;
align-items: stretch;
}
.image-wrapper img {
width: 100%;
height: 100%;
min-width: 0;
}
.secondary-section .g .section .component-wrapper .first-column {
padding-right: 100px;
flex: 0 1 auto;
height: auto;
width: 50%;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper .first-column {
padding-right: 0px;
width: 100%;
margin-bottom: 32px;
}
}
.secondary-section .g .section .component-wrapper .second-column {
flex: 0 1 auto;
height: auto;
width: 50%;
}
@media screen and (max-width: 1012px) {
.secondary-section .g .section .component-wrapper .second-column {
width: 100%;
margin-bottom: 32px;
}
}
.secondary-section .g .section .component-wrapper .responsive-grid {
display: grid;
width: 100%;
grid-template-columns: repeat(1, 1fr);
gap: 2rem;
}
@media screen and (min-width: 64rem) {
.secondary-section .g .section .component-wrapper .responsive-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.secondary-section .g .section .component-wrapper .responsive-grid a.card-wrapper {
text-decoration: none;
transition: none;
background: none;
padding: 0;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card {
position: relative;
background-color: #fff none repeat scroll 0% 0%;
padding: 1.5rem;
display: flex;
flex-direction: row;
-moz-box-align: center;
align-items: center;
height: 100%;
-moz-box-pack: start;
justify-content: flex-start;
box-shadow: rgba(0, 0, 0, 0.09) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.15) 0px 0.25rem 0.5rem 0px;
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) 0s;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card:hover {
box-shadow: rgba(0, 0, 0, 0.2) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.26) 0px 0.25rem 0.5rem 0px;
}
@media screen and (min-width: 75rem) {
.secondary-section .g .section .component-wrapper .responsive-grid .card {
padding: 2rem 2.5rem;
}
}
@media screen and (min-width: 36rem) {
.secondary-section .g .section .component-wrapper .responsive-grid .card {
padding: 1rem 1.5rem;
}
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .logo {
margin-right: 0.75rem;
width: 1.2rem;
min-width: 1.2rem;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content {
display: flex;
flex: 1 1 0%;
flex-direction: column;
width: 100%;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content h5 {
color: rgb(61, 61, 61);
margin: 0;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content p {
margin-top: 0.25em;
margin-bottom: 0;
color: rgb(92, 92, 92);
font-size: 0.65rem;
font-weight: 300;
line-height: normal;
}
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content code {
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
padding: 2px 6px;
border-radius: 4px;
}
.component-wrapper span.em {
color: rgb(61, 61, 61);
}
.component-wrapper a {
transition: color 125ms;
color: rgb(61, 61, 61);
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
padding: 2px 6px;
margin: 0px 1px;
border-radius: 4px;
display: inline;
cursor: pointer;
font-weight: 600;
}
.component-wrapper a:hover {
color: var(--md-typeset-a-color);
background: var(--md-accent-fg-color--transparent);
}
</style>
<!-- Hero for landing page -->
<div class="md-container tx-hero">
<div class="md-grid md-typeset">
<div class="md-main__inner">
<div>
<h1>A beautiful and simple UI to manage your WireGuard peers and interfaces</h1>
<p>WireGuard Portal is an open source web-based user interface that makes it easy to setup and manage
WireGuard VPN connections. It's built on top of WireGuard's official <span class="em">wgctrl</span> library.</p>
</p>
<a
href="documentation/overview/"
title="Get Started"
class="md-button md-button--primary"
>
Get started
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" style="margin-left:2px"><path d="M1 5.16772H9.5M9.5 5.16772L6.5 1.66772M9.5 5.16772L6.5 8.66772" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
</a>
</div>
</div>
</div>
</div>
<div class="md-container">
<div class="tx-hero__image">
<img
src="{{config.site_url}}assets/images/screenshot.png"
alt=""
draggable="false"
>
</div>
</div>
<div class="md-container secondary-section">
<div class="g">
<!-- Architecture as building blocks -->
<div class="section">
<div class="component-wrapper">
<div class="first-column">
<h3>More information about WireGuard</h3>
<p>
WireGuard® is an extremely <span class="em">simple</span> yet <span class="em">fast</span> and modern
VPN that utilizes <span class="em">state-of-the-art cryptography</span>.
</p>
<p>
WireGuard uses state-of-the-art <a href="https://www.wireguard.com/protocol/">cryptography</a> and still
manages to be as easy to configure and deploy as SSH.
A combination of extremely high-speed cryptographic primitives and the fact that WireGuard lives inside
the Linux kernel means that secure networking can be very high-speed.
It is suitable for both small embedded devices like smartphones and fully loaded backbone routers.
</p>
</div>
<div class="second-column">
<div class="image-wrapper">
<img
src="{{config.site_url}}assets/images/wg-tool.png"
alt=""
draggable="false"
>
</div>
</div>
</div>
<div class="component-wrapper" style="display: block;">
<h4>Explore official documentation</h4>
<!-- Arch as code -->
<div class="responsive-grid">
<a class="card-wrapper" href="https://www.wireguard.com/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/octicons/file-code-24.svg" %}
</span>
</div>
<div class="card-content">
<h5>Official Website</h5>
<p>
If you'd like a general conceptual overview of what WireGuard is about, read onward here.
</p>
</div>
</div>
</a>
<!-- Networking -->
<a class="card-wrapper" href="https://www.wireguard.com/protocol/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/fontawesome/solid/network-wired.svg" %}
</span>
</div>
<div class="card-content">
<h5>Protocol & Cryptography</h5>
<p>
WireGuard uses state-of-the-art cryptography, like the Noise protocol framework, Curve25519, ChaCha20, Poly1305, BLAKE2, SipHash24, HKDF, and secure trusted constructions.
</p>
</div>
</div>
</a>
<!-- Customize -->
<a class="card-wrapper" href="https://www.wireguard.com/install/">
<div class="card">
<div class="logo">
<span class="twemoji">
{% include ".icons/fontawesome/solid/puzzle-piece.svg" %}
</span>
</div>
<div class="card-content">
<h5>Client Installation</h5>
<p>
You may progress to installation and reading the quickstart instructions on how to use it.
</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
<!-- Content -->
{% block content %}{% endblock %}
<!-- Application footer -->
{% block footer %}
{{ super() }}
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% 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 %}

View File

@@ -1,32 +0,0 @@
{% import "partials/language.html" as lang with context %}
<!-- Application footer -->
<footer class="md-footer">
<!-- Further information -->
<div class="md-footer-meta md-typeset" style="background-color: #fff;">
<div class="md-footer-meta__inner md-grid" style="background-color: #fff;">
<!-- Copyright and theme information -->
<div class="md-footer-copyright">
{% if config.copyright %}
<div class="md-footer-copyright__highlight" style="color: rgb(38, 38, 38);">
{{ config.copyright }}
</div>
{% endif %}
<div style="color: rgb(38, 38, 38);">
Made with
<a
href="https://squidfunk.github.io/mkdocs-material/"
target="_blank" rel="noopener"
style="color: black;"
>
Material for MkDocs
</a>
</div>
</div>
<!-- Social links -->
{% include "partials/social.html" %}
</div>
</div>
</footer>

12
efs.go Normal file
View File

@@ -0,0 +1,12 @@
package wg_portal
import "embed"
//go:embed assets/tpl/*
var Templates embed.FS
//go:embed assets/css/*
//go:embed assets/fonts/*
//go:embed assets/img/*
//go:embed assets/js/*
var Statics embed.FS

View File

@@ -1 +0,0 @@
VITE_SOME_EXAMPLE_VAR=http://localhost:5000 (can be used internally like: import.meta.env.VITE_SOME_EXAMPLE_VAR)

View File

@@ -1 +0,0 @@
VITE_API_BASE_URL=https://wgportal.server.com

28
frontend/.gitignore vendored
View File

@@ -1,28 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/extensions.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
}

View File

@@ -1,29 +0,0 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link href="/favicon.ico" rel="icon" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>WireGuard Portal</title>
<meta content="WireGuard VPN Management Portal" name="description">
<script>
// global config, will be overridden by backend if available
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
let WGPORTAL_VERSION="unknown";
let WGPORTAL_SITE_TITLE="WireGuard Portal";
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
</script>
<script src="/api/v0/config/frontend.js"></script>
</head>
<body class="d-flex flex-column min-vh-100">
<noscript>
<strong>We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- vue teleport will add toasts here -->
<div id="toasts"></div>
<!-- main application -->
<div id="app"></div>
<!-- vue teleport will add modals and dialogs here -->
<div id="modals"></div>
<div id="dialogs"></div>
<script src="/src/main.js" type="module"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build-dev": "vite build --mode development --base=/app/",
"build": "vite build --base=/app/",
"preview": "vite preview --port 5050"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@kyvg/vue3-notification": "^3.1.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.2",
"bootswatch": "^5.3.2",
"flag-icons": "^7.1.0",
"is-cidr": "^5.0.3",
"is-ip": "^5.0.1",
"pinia": "^2.1.7",
"prismjs": "^1.29.0",
"vue": "^3.3.13",
"vue-i18n": "^9.8.0",
"vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^4.2.5",
"vue3-tags-input": "^1.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

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