Compare commits
6 Commits
v2.0.0-alp
...
v1.0.19
Author | SHA1 | Date | |
---|---|---|---|
|
2f79dd04c0 | ||
|
e5ed9736b3 | ||
|
c8353b85ae | ||
|
6142031387 | ||
|
dd86d0ff49 | ||
|
bdd426a679 |
@@ -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.*/
|
28
.github/workflows/docker-publish.yml
vendored
@@ -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' }}
|
||||
|
7
.gitignore
vendored
@@ -28,11 +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.yml
|
||||
sqlite.db
|
||||
node_modules/
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
13
Dockerfile
@@ -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
|
||||
@@ -42,15 +42,12 @@ 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" ]
|
||||
|
@@ -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
|
||||
|
45
Makefile
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
# WireGuard Portal (v2 - testing)
|
||||
# WireGuard Portal (v1)
|
||||
|
||||
[](https://travis-ci.com/h44z/wg-portal)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
@@ -6,203 +6,255 @@
|
||||
[](https://goreportcard.com/report/github.com/h44z/wg-portal)
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
||||
|
||||
> :warning: **IMPORTANT** Version 2 is currently under development and may contain bugs. It is currently not advised to use this version
|
||||
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
||||
|
||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
|
||||
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||
[](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)
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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;
|
5
assets/css/bootstrap-tokenfield.min.css
vendored
Normal 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
@@ -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
8
assets/css/signin.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.navbar {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.navbar-brand > img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
5
assets/css/tokenfield-typeahead.min.css
vendored
Normal 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}
|
Before Width: | Height: | Size: 692 KiB After Width: | Height: | Size: 692 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 829 KiB After Width: | Height: | Size: 829 KiB |
4
assets/fonts/font-awesome.min.css
vendored
Normal file
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
13
assets/js/bootstrap-confirmation.min.js
vendored
Normal file
7
assets/js/bootstrap-tokenfield.min.js
vendored
Normal file
39
assets/js/custom.js
Normal 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
73
assets/tpl/admin_create_clients.html
Normal 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>
|
221
assets/tpl/admin_edit_client.html
Normal 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>
|
263
assets/tpl/admin_edit_interface.html
Normal 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>
|
95
assets/tpl/admin_edit_user.html
Normal 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>
|
278
assets/tpl/admin_index.html
Normal file
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Administration</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
|
||||
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
|
||||
|
||||
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
|
||||
|
||||
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Endpoint:</td>
|
||||
<td>{{.Device.DefaultEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Listening Port:</td>
|
||||
<td>{{.Device.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Peers:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Peers:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default allowed IP's:</td>
|
||||
<td>{{.Device.DefaultAllowedIPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default Keepalive Interval:</td>
|
||||
<td>{{.Device.DefaultPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Endpoints:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Endpoints:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<h2 class="mt-2">Current VPN Peers</h2>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<h2 class="mt-2">Current VPN Endpoints</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
|
||||
{{end}}
|
||||
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
|
||||
{{end}}
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.Email}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<td>{{$p.Endpoint}}</td>
|
||||
{{end}}
|
||||
<td><span data-toggle="tooltip" data-placement="left" title="" data-original-title="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</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>
|
69
assets/tpl/admin_user_index.html
Normal 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>
|
@@ -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
@@ -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
@@ -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
@@ -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>
|
5
assets/tpl/prt_flashes.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{{range $flash := $.Alerts}}
|
||||
<div class="alert alert-{{$flash.Type}}" role="alert">
|
||||
{{$flash.Message}}
|
||||
</div>
|
||||
{{end}}
|
5
assets/tpl/prt_footer.html
Normal 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
@@ -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}}
|
44
assets/tpl/user_create_client.html
Normal 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>
|
54
assets/tpl/user_edit_client.html
Normal 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>
|
137
assets/tpl/user_index.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<!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="{{$p.LastHandshakeTime}}">{{$p.LastHandshake}}</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>
|
@@ -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
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
@@ -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
|
||||
|
12
efs.go
Normal 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
|
@@ -1 +0,0 @@
|
||||
VITE_SOME_EXAMPLE_VAR=http://localhost:5000 (can be used internally like: import.meta.env.VITE_SOME_EXAMPLE_VAR)
|
@@ -1 +0,0 @@
|
||||
VITE_API_BASE_URL=https://wgportal.server.com
|
28
frontend/.gitignore
vendored
@@ -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?
|
3
frontend/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
|
||||
}
|
@@ -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
|
||||
```
|
@@ -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>
|
1684
frontend/package-lock.json
generated
@@ -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.4.0",
|
||||
"@kyvg/vue3-notification": "^2.9.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.0",
|
||||
"bootswatch": "^5.3.0",
|
||||
"flag-icons": "^6.7.0",
|
||||
"is-cidr": "^5.0.3",
|
||||
"is-ip": "^5.0.0",
|
||||
"pinia": "^2.1.4",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
@@ -1,128 +0,0 @@
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import {computed, getCurrentInstance, onMounted, ref} from "vue";
|
||||
import {authStore} from "./stores/auth";
|
||||
import {securityStore} from "./stores/security";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
||||
const auth = authStore()
|
||||
const sec = securityStore()
|
||||
const settings = settingsStore()
|
||||
|
||||
onMounted(async () => {
|
||||
console.log("Starting WireGuard Portal frontend...");
|
||||
|
||||
await sec.LoadSecurityProperties();
|
||||
await auth.LoadProviders();
|
||||
|
||||
let wasLoggedIn = auth.IsAuthenticated;
|
||||
try {
|
||||
await auth.LoadSession();
|
||||
await settings.LoadSettings(); // only logs errors, does not throw
|
||||
|
||||
console.log("WireGuard Portal session is valid");
|
||||
} catch (e) {
|
||||
if (wasLoggedIn) {
|
||||
console.log("WireGuard Portal invalid - logging out");
|
||||
await auth.Logout();
|
||||
}
|
||||
}
|
||||
|
||||
console.log("WireGuard Portal ready!");
|
||||
})
|
||||
|
||||
const switchLanguage = function (lang) {
|
||||
if (appGlobal.$i18n.locale !== lang) {
|
||||
localStorage.setItem('wgLang', lang);
|
||||
appGlobal.$i18n.locale = lang;
|
||||
}
|
||||
}
|
||||
|
||||
const languageFlag = computed(() => {
|
||||
// `this` points to the component instance
|
||||
let lang = appGlobal.$i18n.locale.toLowerCase();
|
||||
if (lang === "en") {
|
||||
lang = "us";
|
||||
}
|
||||
return "fi-" + lang;
|
||||
})
|
||||
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<notifications :duration="3000" :ignore-duplicates="true" position="top right" />
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/"><img alt="WireGuard Portal" src="/img/header-logo.png" /></a>
|
||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div id="navbarTop" class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'home' }" class="nav-link">{{ $t('menu.home') }}</RouterLink>
|
||||
</li>
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'interfaces' }" class="nav-link">{{ $t('menu.interfaces') }}</RouterLink>
|
||||
</li>
|
||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"
|
||||
role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
||||
<div class="dropdown-menu">
|
||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout">
|
||||
<i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!auth.IsAuthenticated" class="nav-item">
|
||||
<RouterLink :to="{ name: 'login' }" class="nav-link">
|
||||
<i class="fas fa-sign-in-alt fa-sm fa-fw me-2"></i>{{ $t('menu.login') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-5 flex-shrink-0">
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
<footer class="page-footer mt-auto">
|
||||
<div class="container mt-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-6">Copyright © {{ companyName }} {{ currentYear }} <span v-if="auth.IsAuthenticated"> - version {{ wgVersion }}</span></div>
|
||||
<div class="col-6 text-end">
|
||||
<div :aria-label="$t('menu.lang')" class="btn-group" role="group">
|
||||
<div class="btn-group" role="group">
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn btn-secondary pe-0" data-bs-toggle="dropdown" type="button"><span :class="languageFlag" class="fi"></span></button>
|
||||
<div aria-labelledby="btnGroupDrop3" class="dropdown-menu" style="">
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
@@ -1,5 +0,0 @@
|
||||
a.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: #888888;
|
||||
}
|
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
Before Width: | Height: | Size: 308 B |
@@ -1,54 +0,0 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const title = ref("Default Title")
|
||||
const question = ref("Default Question")
|
||||
const visible = ref(true)
|
||||
|
||||
const emit = defineEmits(['no', 'yes'])
|
||||
|
||||
function showDialog(titleStr, questionStr) {
|
||||
visible.value = true
|
||||
title.value = titleStr
|
||||
question.value = questionStr
|
||||
}
|
||||
|
||||
function yes() {
|
||||
visible.value = false
|
||||
emit('yes')
|
||||
}
|
||||
|
||||
function no() {
|
||||
visible.value = false
|
||||
emit('no')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="#dialogs">
|
||||
<div v-if="visible" class="modal-backdrop fade show">
|
||||
<div class="modal fade show" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-scrollable" @click.stop="">
|
||||
<div class="modal-content" ref="body">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ question }}
|
||||
</div>
|
||||
<div class="modal-footer pt-0 border-top-0">
|
||||
<button type="button" class="btn btn-primary" @click="no">{{ $t('general.no') }}</button>
|
||||
<button type="button" class="btn btn-success" @click="yes">{{ $t('general.yes') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
@@ -1,513 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from 'vue3-tags-input';
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
import {peerStore} from "@/stores/peers";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
const peers = peerStore()
|
||||
|
||||
const props = defineProps({
|
||||
interfaceId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.Find(props.interfaceId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
|
||||
if (selectedInterface.value) {
|
||||
return t("modals.interface-edit.headline-edit") + " " + selectedInterface.value.Identifier
|
||||
}
|
||||
return t("modals.interface-edit.headline-new")
|
||||
})
|
||||
|
||||
const formData = ref(freshInterface())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
if (!selectedInterface.value) {
|
||||
await interfaces.PrepareInterface()
|
||||
|
||||
// fill form data
|
||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||
formData.value.Mode = interfaces.Prepared.Mode
|
||||
|
||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||
formData.value.PrivateKey = interfaces.Prepared.PrivateKey
|
||||
|
||||
formData.value.ListenPort = interfaces.Prepared.ListenPort
|
||||
formData.value.Addresses = interfaces.Prepared.Addresses
|
||||
formData.value.Dns = interfaces.Prepared.Dns
|
||||
formData.value.DnsSearch = interfaces.Prepared.DnsSearch
|
||||
|
||||
formData.value.Mtu = interfaces.Prepared.Mtu
|
||||
formData.value.FirewallMark = interfaces.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = interfaces.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = interfaces.Prepared.PreUp
|
||||
formData.value.PostUp = interfaces.Prepared.PostUp
|
||||
formData.value.PreDown = interfaces.Prepared.PreDown
|
||||
formData.value.PostDown = interfaces.Prepared.PostDown
|
||||
|
||||
formData.value.SaveConfig = interfaces.Prepared.SaveConfig
|
||||
|
||||
formData.value.PeerDefNetwork = interfaces.Prepared.PeerDefNetwork
|
||||
formData.value.PeerDefDns = interfaces.Prepared.PeerDefDns
|
||||
formData.value.PeerDefDnsSearch = interfaces.Prepared.PeerDefDnsSearch
|
||||
formData.value.PeerDefEndpoint = interfaces.Prepared.PeerDefEndpoint
|
||||
formData.value.PeerDefAllowedIPs = interfaces.Prepared.PeerDefAllowedIPs
|
||||
formData.value.PeerDefMtu = interfaces.Prepared.PeerDefMtu
|
||||
formData.value.PeerDefPersistentKeepalive = interfaces.Prepared.PeerDefPersistentKeepalive
|
||||
formData.value.PeerDefFirewallMark = interfaces.Prepared.PeerDefFirewallMark
|
||||
formData.value.PeerDefRoutingTable = interfaces.Prepared.PeerDefRoutingTable
|
||||
formData.value.PeerDefPreUp = interfaces.Prepared.PeerDefPreUp
|
||||
formData.value.PeerDefPostUp = interfaces.Prepared.PeerDefPostUp
|
||||
formData.value.PeerDefPreDown = interfaces.Prepared.PeerDefPreDown
|
||||
formData.value.PeerDefPostDown = interfaces.Prepared.PeerDefPostDown
|
||||
} else { // fill existing userdata
|
||||
formData.value.Disabled = selectedInterface.value.Disabled
|
||||
formData.value.Identifier = selectedInterface.value.Identifier
|
||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||
formData.value.Mode = selectedInterface.value.Mode
|
||||
|
||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||
formData.value.PrivateKey = selectedInterface.value.PrivateKey
|
||||
|
||||
formData.value.ListenPort = selectedInterface.value.ListenPort
|
||||
formData.value.Addresses = selectedInterface.value.Addresses
|
||||
formData.value.Dns = selectedInterface.value.Dns
|
||||
formData.value.DnsSearch = selectedInterface.value.DnsSearch
|
||||
|
||||
formData.value.Mtu = selectedInterface.value.Mtu
|
||||
formData.value.FirewallMark = selectedInterface.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedInterface.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedInterface.value.PreUp
|
||||
formData.value.PostUp = selectedInterface.value.PostUp
|
||||
formData.value.PreDown = selectedInterface.value.PreDown
|
||||
formData.value.PostDown = selectedInterface.value.PostDown
|
||||
|
||||
formData.value.SaveConfig = selectedInterface.value.SaveConfig
|
||||
|
||||
formData.value.PeerDefNetwork = selectedInterface.value.PeerDefNetwork
|
||||
formData.value.PeerDefDns = selectedInterface.value.PeerDefDns
|
||||
formData.value.PeerDefDnsSearch = selectedInterface.value.PeerDefDnsSearch
|
||||
formData.value.PeerDefEndpoint = selectedInterface.value.PeerDefEndpoint
|
||||
formData.value.PeerDefAllowedIPs = selectedInterface.value.PeerDefAllowedIPs
|
||||
formData.value.PeerDefMtu = selectedInterface.value.PeerDefMtu
|
||||
formData.value.PeerDefPersistentKeepalive = selectedInterface.value.PeerDefPersistentKeepalive
|
||||
formData.value.PeerDefFirewallMark = selectedInterface.value.PeerDefFirewallMark
|
||||
formData.value.PeerDefRoutingTable = selectedInterface.value.PeerDefRoutingTable
|
||||
formData.value.PeerDefPreUp = selectedInterface.value.PeerDefPreUp
|
||||
formData.value.PeerDefPostUp = selectedInterface.value.PeerDefPostUp
|
||||
formData.value.PeerDefPreDown = selectedInterface.value.PeerDefPreDown
|
||||
formData.value.PeerDefPostDown = selectedInterface.value.PeerDefPostDown
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshInterface()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch = tags
|
||||
}
|
||||
|
||||
function handleChangePeerDefNetwork(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefNetwork = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefAllowedIPs = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.PeerDefDns = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePeerDefDnsSearch(tags) {
|
||||
formData.value.PeerDefDnsSearch = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.interfaceId!=='#NEW#') {
|
||||
await interfaces.UpdateInterface(selectedInterface.value.Identifier, formData.value)
|
||||
} else {
|
||||
await interfaces.CreateInterface(formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to save interface!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPeerDefaults() {
|
||||
if (props.interfaceId==='#NEW#') {
|
||||
return; // do nothing for new interfaces
|
||||
}
|
||||
|
||||
try {
|
||||
await interfaces.ApplyPeerDefaults(selectedInterface.value.Identifier, formData.value)
|
||||
|
||||
notify({
|
||||
title: "Peer Defaults Applied",
|
||||
text: "Applied current peer defaults to all available peers.",
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
await peers.LoadPeers(selectedInterface.value.Identifier) // reload all peers after applying the defaults
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to apply peer defaults!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to delete interface!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#interface">{{ $t('modals.interface-edit.tab-interface') }}</a>
|
||||
</li>
|
||||
<li v-if="formData.Mode==='server'" class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#peerdefaults">{{ $t('modals.interface-edit.tab-peerdef') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="interfaceTabs" class="tab-content">
|
||||
<div id="interface" class="tab-pane fade active show">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-general') }}</legend>
|
||||
<div v-if="props.interfaceId==='#NEW#'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.identifier.label') }}</label>
|
||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.interface-edit.identifier.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mode.label') }}</label>
|
||||
<select v-model="formData.Mode" class="form-select">
|
||||
<option value="server">{{ $t('modals.interface-edit.mode.server') }}</option>
|
||||
<option value="client">{{ $t('modals.interface-edit.mode.client') }}</option>
|
||||
<option value="any">{{ $t('modals.interface-edit.mode.any') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.display-name.label') }}</label>
|
||||
<input v-model="formData.DisplayName" class="form-control" :placeholder="$t('modals.interface-edit.display-name.placeholder')" type="text">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode==='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
||||
<input v-model="formData.ListenPort" class="form-control" :placeholder="$t('modals.interface-edit.listen-port.placeholder')" type="number">
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
</div>
|
||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.mtu.label') }}</label>
|
||||
<input v-model="formData.Mtu" class="form-control" :placeholder="$t('modals.interface-edit.mtu.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||
<input v-model="formData.FirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||
<input v-model="formData.RoutingTable" aria-describedby="routingTableHelp" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="text">
|
||||
<small id="routingTableHelp" class="form-text text-muted">{{ $t('modals.interface-edit.routing-table.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-state') }}</legend>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div id="peerdefaults" class="tab-pane fade">
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.endpoint.label') }}</label>
|
||||
<input v-model="formData.PeerDefEndpoint" class="form-control" :placeholder="$t('modals.interface-edit.defaults.endpoint.placeholder')" type="text">
|
||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.endpoint.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
|
||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefNetwork"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
|
||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
|
||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangePeerDefDns"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
|
||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangePeerDefDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.mtu.label') }}</label>
|
||||
<input v-model="formData.PeerDefMtu" class="form-control" :placeholder="$t('modals.interface-edit.defaults.mtu.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.firewall-mark.label') }}</label>
|
||||
<input v-model="formData.PeerDefFirewallMark" class="form-control" :placeholder="$t('modals.interface-edit.firewall-mark.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.routing-table.label') }}</label>
|
||||
<input v-model="formData.PeerDefRoutingTable" class="form-control" :placeholder="$t('modals.interface-edit.routing-table.placeholder')" type="number">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.keep-alive.label') }}</label>
|
||||
<input v-model="formData.PeerDefPersistentKeepalive" class="form-control" :placeholder="$t('modals.interface-edit.defaults.keep-alive.placeholder')" type="number">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-peer-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPreUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPostUp" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPreDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PeerDefPostDown" class="form-control" rows="2" :placeholder="$t('modals.interface-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="props.interfaceId!=='#NEW#'" class="text-end">
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="applyPeerDefaults">{{ $t('modals.interface-edit.button-apply-defaults') }}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
@@ -1,60 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import Prism from 'vue-prism-component'
|
||||
import 'prismjs/components/prism-ini'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
interfaceId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const configString = ref("")
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
return interfaces.Find(props.interfaceId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
|
||||
return t("modals.interface-view.headline") + " " + selectedInterface.value.Identifier
|
||||
})
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
await interfaces.LoadInterfaceConfig(selectedInterface.value.Identifier)
|
||||
configString.value = interfaces.configuration
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<Prism language="ini" :code="configString"></Prism>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<Teleport to="#modals">
|
||||
<div v-show="visible" class="modal-backdrop fade show" @click="closeBackdrop">
|
||||
<div class="modal fade show" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" @click.stop="">
|
||||
<div class="modal-content" ref="body">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
<button @click="closeModal" class="btn-close" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body col-md-12">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.modal.show {
|
||||
display:block;
|
||||
}
|
||||
.modal.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.modal-backdrop {
|
||||
background-color: rgba(0,0,0,0.6) !important;
|
||||
}
|
||||
.modal-backdrop.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
closeOnBackdrop: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
function closeBackdrop() {
|
||||
if(props.closeOnBackdrop) {
|
||||
console.log("CLOSING BD")
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
console.log("CLOSING")
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
@@ -1,431 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from 'is-ip';
|
||||
import { freshPeer, freshInterface } from '@/helpers/models';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
return peers.Find(props.peerId)
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
if (selectedPeer.value) {
|
||||
return t("modals.peer-edit.headline-edit-peer") + " " + selectedPeer.value.Identifier
|
||||
}
|
||||
return t("modals.peer-edit.headline-new-peer")
|
||||
} else {
|
||||
if (selectedPeer.value) {
|
||||
return t("modals.peer-edit.headline-edit-endpoint") + " " + selectedPeer.value.Identifier
|
||||
}
|
||||
return t("modals.peer-edit.headline-new-endpoint")
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref(freshPeer())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
console.log(selectedInterface.value)
|
||||
console.log(selectedPeer.value)
|
||||
if (!selectedPeer.value) {
|
||||
await peers.PreparePeer(selectedInterface.value.Identifier)
|
||||
|
||||
formData.value.Identifier = peers.Prepared.Identifier
|
||||
formData.value.DisplayName = peers.Prepared.DisplayName
|
||||
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
|
||||
formData.value.Disabled = peers.Prepared.Disabled
|
||||
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
|
||||
formData.value.Notes = peers.Prepared.Notes
|
||||
|
||||
formData.value.Endpoint = peers.Prepared.Endpoint
|
||||
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
|
||||
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = peers.Prepared.PresharedKey
|
||||
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = peers.Prepared.PrivateKey
|
||||
formData.value.PublicKey = peers.Prepared.PublicKey
|
||||
|
||||
formData.value.Mode = peers.Prepared.Mode
|
||||
|
||||
formData.value.Addresses = peers.Prepared.Addresses
|
||||
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
|
||||
formData.value.Dns = peers.Prepared.Dns
|
||||
formData.value.DnsSearch = peers.Prepared.DnsSearch
|
||||
formData.value.Mtu = peers.Prepared.Mtu
|
||||
formData.value.FirewallMark = peers.Prepared.FirewallMark
|
||||
formData.value.RoutingTable = peers.Prepared.RoutingTable
|
||||
|
||||
formData.value.PreUp = peers.Prepared.PreUp
|
||||
formData.value.PostUp = peers.Prepared.PostUp
|
||||
formData.value.PreDown = peers.Prepared.PreDown
|
||||
formData.value.PostDown = peers.Prepared.PostDown
|
||||
|
||||
} else { // fill existing data
|
||||
formData.value.Identifier = selectedPeer.value.Identifier
|
||||
formData.value.DisplayName = selectedPeer.value.DisplayName
|
||||
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
|
||||
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
|
||||
formData.value.Disabled = selectedPeer.value.Disabled
|
||||
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
|
||||
formData.value.Notes = selectedPeer.value.Notes
|
||||
|
||||
formData.value.Endpoint = selectedPeer.value.Endpoint
|
||||
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
|
||||
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
|
||||
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
|
||||
formData.value.PresharedKey = selectedPeer.value.PresharedKey
|
||||
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
|
||||
|
||||
formData.value.PrivateKey = selectedPeer.value.PrivateKey
|
||||
formData.value.PublicKey = selectedPeer.value.PublicKey
|
||||
|
||||
formData.value.Mode = selectedPeer.value.Mode
|
||||
|
||||
formData.value.Addresses = selectedPeer.value.Addresses
|
||||
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
|
||||
formData.value.Dns = selectedPeer.value.Dns
|
||||
formData.value.DnsSearch = selectedPeer.value.DnsSearch
|
||||
formData.value.Mtu = selectedPeer.value.Mtu
|
||||
formData.value.FirewallMark = selectedPeer.value.FirewallMark
|
||||
formData.value.RoutingTable = selectedPeer.value.RoutingTable
|
||||
|
||||
formData.value.PreUp = selectedPeer.value.PreUp
|
||||
formData.value.PostUp = selectedPeer.value.PostUp
|
||||
formData.value.PreDown = selectedPeer.value.PreDown
|
||||
formData.value.PostDown = selectedPeer.value.PostDown
|
||||
|
||||
if (!formData.value.Endpoint.Overridable ||
|
||||
!formData.value.EndpointPublicKey.Overridable ||
|
||||
!formData.value.AllowedIPs.Overridable ||
|
||||
!formData.value.PersistentKeepalive.Overridable ||
|
||||
!formData.value.Dns.Overridable ||
|
||||
!formData.value.DnsSearch.Overridable ||
|
||||
!formData.value.Mtu.Overridable ||
|
||||
!formData.value.FirewallMark.Overridable ||
|
||||
!formData.value.RoutingTable.Overridable ||
|
||||
!formData.value.PreUp.Overridable ||
|
||||
!formData.value.PostUp.Overridable ||
|
||||
!formData.value.PreDown.Overridable ||
|
||||
!formData.value.PostDown.Overridable) {
|
||||
formData.value.IgnoreGlobalSettings = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.IgnoreGlobalSettings, async (newValue, oldValue) => {
|
||||
formData.value.Endpoint.Overridable = !newValue
|
||||
formData.value.EndpointPublicKey.Overridable = !newValue
|
||||
formData.value.AllowedIPs.Overridable = !newValue
|
||||
formData.value.PersistentKeepalive.Overridable = !newValue
|
||||
formData.value.Dns.Overridable = !newValue
|
||||
formData.value.DnsSearch.Overridable = !newValue
|
||||
formData.value.Mtu.Overridable = !newValue
|
||||
formData.value.FirewallMark.Overridable = !newValue
|
||||
formData.value.RoutingTable.Overridable = !newValue
|
||||
formData.value.PreUp.Overridable = !newValue
|
||||
formData.value.PostUp.Overridable = !newValue
|
||||
formData.value.PreDown.Overridable = !newValue
|
||||
formData.value.PostDown.Overridable = !newValue
|
||||
}
|
||||
)
|
||||
|
||||
watch(() => formData.value.Disabled, async (newValue, oldValue) => {
|
||||
if (oldValue && !newValue && formData.value.ExpiresAt) {
|
||||
formData.value.ExpiresAt = "" // reset expiry date
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshPeer()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeAddresses(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Addresses = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.AllowedIPs.Value = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeExtraAllowedIPs(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(isCidr(tag) === 0) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid CIDR",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.ExtraAllowedIPs = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDns(tags) {
|
||||
let validInput = true
|
||||
tags.forEach(tag => {
|
||||
if(!isIP(tag)) {
|
||||
validInput = false
|
||||
notify({
|
||||
title: "Invalid IP",
|
||||
text: tag + " is not a valid IP address",
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
if(validInput) {
|
||||
formData.value.Dns.Value = tags
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangeDnsSearch(tags) {
|
||||
formData.value.DnsSearch.Value = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.peerId!=='#NEW#') {
|
||||
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
|
||||
} else {
|
||||
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to save peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await peers.DeletePeer(selectedPeer.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to delete peer!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-general') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.display-name.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.display-name.placeholder')" v-model="formData.DisplayName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.linked-user.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.linked-user.placeholder')" v-model="formData.UserIdentifier">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='server'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required v-model="formData.PrivateKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required v-model="formData.PublicKey">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')" v-model="formData.PresharedKey">
|
||||
</div>
|
||||
<div class="form-group" v-if="formData.Mode==='client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint-public-key.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint-public-key.placeholder')" v-model="formData.EndpointPublicKey.Value">
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-network') }}</legend>
|
||||
<div class="form-group" v-if="selectedInterface.Mode==='client'">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.endpoint.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.endpoint.placeholder')" v-model="formData.Endpoint.Value">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
||||
:placeholder="$t('modals.peer-edit.ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAddresses"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
|
||||
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeAllowedIPs"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
|
||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateCIDR"
|
||||
@on-tags-changed="handleChangeExtraAllowedIPs"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Dns.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateIP"
|
||||
@on-tags-changed="handleChangeDns"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
|
||||
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
:validate="validateDomain"
|
||||
@on-tags-changed="handleChangeDnsSearch"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.keep-alive.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.keep-alive.label')" v-model="formData.PersistentKeepalive.Value">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.mtu.label') }}</label>
|
||||
<input type="number" class="form-control" :placeholder="$t('modals.peer-edit.mtu.label')" v-model="formData.Mtu.Value">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-hooks') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-up.label') }}</label>
|
||||
<textarea v-model="formData.PreUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-up.label') }}</label>
|
||||
<textarea v-model="formData.PostUp.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-up.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.pre-down.label') }}</label>
|
||||
<textarea v-model="formData.PreDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.pre-down.placeholder')"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.post-down.label') }}</label>
|
||||
<textarea v-model="formData.PostDown.Value" class="form-control" rows="2" :placeholder="$t('modals.peer-edit.post-down.placeholder')"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-state') }}</legend>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||
<label class="form-check-label" >{{ $t('modals.peer-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" v-model="formData.IgnoreGlobalSettings">
|
||||
<label class="form-check-label">{{ $t('modals.peer-edit.ignore-global.label') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label">{{ $t('modals.peer-edit.expires-at.label') }}</label>
|
||||
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01" v-model="formData.ExpiresAt">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.peerId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
@@ -1,110 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import Vue3TagsInput from "vue3-tags-input";
|
||||
import { freshInterface } from '@/helpers/models';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
function freshForm() {
|
||||
return {
|
||||
Identifiers: [],
|
||||
Suffix: "",
|
||||
}
|
||||
}
|
||||
|
||||
const formData = ref(freshForm())
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
return t("modals.peer-multi-create.headline-peer")
|
||||
} else {
|
||||
return t("modals.peer-multi-create.headline-endpoint")
|
||||
}
|
||||
})
|
||||
|
||||
function close() {
|
||||
formData.value = freshForm()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function handleChangeUserIdentifiers(tags) {
|
||||
formData.value.Identifiers = tags
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (formData.value.Identifiers.length === 0) {
|
||||
notify({
|
||||
title: "Missing Identifiers",
|
||||
text: "At least one identifier is required to create a new peer.",
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await peers.CreateMultiplePeers(selectedInterface.value.Identifier, formData.value)
|
||||
close()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
notify({
|
||||
title: "Failed to create peers!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
||||
<vue3-tags-input class="form-control" :tags="formData.Identifiers"
|
||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
||||
@on-tags-changed="handleChangeUserIdentifiers"/>
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@@ -1,199 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import {interfaceStore} from "@/stores/interfaces";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {freshInterface, freshPeer, freshStats} from '@/helpers/models';
|
||||
import Prism from "vue-prism-component";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {settingsStore} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const settings = settingsStore()
|
||||
const peers = peerStore()
|
||||
const interfaces = interfaceStore()
|
||||
|
||||
const props = defineProps({
|
||||
peerId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const configString = ref("")
|
||||
|
||||
const selectedPeer = computed(() => {
|
||||
let p = peers.Find(props.peerId)
|
||||
|
||||
if (!p) {
|
||||
p = freshPeer() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return p
|
||||
})
|
||||
|
||||
const selectedStats = computed(() => {
|
||||
let s = peers.Statistics(props.peerId)
|
||||
|
||||
if (!s) {
|
||||
s = freshStats() // dummy peer to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
|
||||
const selectedInterface = computed(() => {
|
||||
let i = interfaces.GetSelected;
|
||||
|
||||
if (!i) {
|
||||
i = freshInterface() // dummy interface to avoid 'undefined' exceptions
|
||||
}
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedInterface.value.Mode === "server") {
|
||||
return t("modals.peer-view.headline-peer") + " " + selectedPeer.value.DisplayName
|
||||
} else {
|
||||
return t("modals.peer-view.headline-endpoint") + " " + selectedPeer.value.DisplayName
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await peers.LoadPeerConfig(selectedPeer.value.Identifier)
|
||||
configString.value = peers.configuration
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function download() {
|
||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||
let filename = 'WireGuard-Tunnel.conf'
|
||||
if (selectedPeer.value.DisplayName) {
|
||||
filename = selectedPeer.value.DisplayName
|
||||
.replace(/ /g,"_")
|
||||
.replace(/[^a-zA-Z0-9-_]/g,"")
|
||||
.substring(0, 16)
|
||||
+ ".conf"
|
||||
}
|
||||
let text = configString.value
|
||||
|
||||
let element = document.createElement('a')
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||||
element.setAttribute('download', filename)
|
||||
|
||||
element.style.display = 'none'
|
||||
document.body.appendChild(element)
|
||||
|
||||
element.click()
|
||||
document.body.removeChild(element)
|
||||
}
|
||||
|
||||
function email() {
|
||||
peers.MailPeerConfig(settings.Setting("MailLinkOnly"), [selectedPeer.value.Identifier]).catch(e => {
|
||||
notify({
|
||||
title: "Failed to send mail with peer configuration!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<div class="accordion" id="peerInformation">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails" aria-expanded="true" aria-controls="collapseDetails">
|
||||
{{ $t('modals.peer-view.section-info') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.identifier') }}: {{ selectedPeer.PublicKey }}</li>
|
||||
<li>{{ $t('modals.peer-view.ip') }}: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
||||
<li>{{ $t('modals.peer-view.user') }}: {{ selectedPeer.UserIdentifier }}</li>
|
||||
<li v-if="selectedPeer.Notes">{{ $t('modals.peer-view.notes') }}: {{ selectedPeer.Notes }}</li>
|
||||
<li v-if="selectedPeer.ExpiresAt">{{ $t('modals.peer-view.expiry-status') }}: {{ selectedPeer.ExpiresAt }}</li>
|
||||
<li v-if="selectedPeer.Disabled">{{ $t('modals.peer-view.disabled-status') }}: {{ selectedPeer.DisabledReason }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingStatus">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
|
||||
{{ $t('modals.peer-view.section-status') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>{{ $t('modals.peer-view.traffic') }}</h4>
|
||||
<p><i class="fas fa-long-arrow-alt-down" :title="$t('modals.peer-view.download')"></i> {{ selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up" :title="$t('modals.peer-view.upload')"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
|
||||
<h4>{{ $t('modals.peer-view.connection-status') }}</h4>
|
||||
<ul>
|
||||
<li>{{ $t('modals.peer-view.pingable') }}: {{ selectedStats.IsPingable }}</li>
|
||||
<li>{{ $t('modals.peer-view.handshake') }}: {{ selectedStats.LastHandshake }}</li>
|
||||
<li>{{ $t('modals.peer-view.connected-since') }}: {{ selectedStats.LastSessionStart }}</li>
|
||||
<li>{{ $t('modals.peer-view.endpoint') }}: {{ selectedStats.EndpointAddress }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingConfig">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
|
||||
{{ $t('modals.peer-view.section-config') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig" data-bs-parent="#peerInformation" style="">
|
||||
<div class="accordion-body">
|
||||
<Prism language="ini" :code="configString"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button @click.prevent="download" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-download') }}</button>
|
||||
<button @click.prevent="email" type="button" class="btn btn-primary me-1">{{ $t('modals.peer-view.button-email') }}</button>
|
||||
</div>
|
||||
<button @click.prevent="close" type="button" class="btn btn-secondary">{{ $t('general.close') }}</button>
|
||||
|
||||
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.config-qr-img {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
@@ -1,174 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {userStore} from "@/stores/users";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import {freshUser} from "@/helpers/models";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const users = userStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedUser = computed(() => {
|
||||
return users.Find(props.userId)
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
if (selectedUser.value) {
|
||||
return t("modals.user-edit.headline-edit") + " " + selectedUser.value.Identifier
|
||||
}
|
||||
return t("modals.user-edit.headline-new")
|
||||
})
|
||||
|
||||
const formData = ref(freshUser())
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
if (!selectedUser.value) {
|
||||
formData.value = freshUser()
|
||||
} else { // fill existing userdata
|
||||
formData.value.Identifier = selectedUser.value.Identifier
|
||||
formData.value.Email = selectedUser.value.Email
|
||||
formData.value.Source = selectedUser.value.Source
|
||||
formData.value.IsAdmin = selectedUser.value.IsAdmin
|
||||
formData.value.Firstname = selectedUser.value.Firstname
|
||||
formData.value.Lastname = selectedUser.value.Lastname
|
||||
formData.value.Phone = selectedUser.value.Phone
|
||||
formData.value.Department = selectedUser.value.Department
|
||||
formData.value.Notes = selectedUser.value.Notes
|
||||
formData.value.Password = ""
|
||||
formData.value.Disabled = selectedUser.value.Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
formData.value = freshUser()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
if (props.userId!=='#NEW#') {
|
||||
await users.UpdateUser(selectedUser.value.Identifier, formData.value)
|
||||
} else {
|
||||
await users.CreateUser(formData.value)
|
||||
}
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to save user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
try {
|
||||
await users.DeleteUser(selectedUser.value.Identifier)
|
||||
close()
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to delete user!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-general') }}</legend>
|
||||
<div v-if="props.userId==='#NEW#'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.identifier.label') }}</label>
|
||||
<input v-model="formData.Identifier" class="form-control" :placeholder="$t('modals.user-edit.identifier.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.source.label') }}</label>
|
||||
<input v-model="formData.Source" class="form-control" disabled="disabled" :placeholder="$t('modals.user-edit.source.placeholder')" type="text">
|
||||
</div>
|
||||
<div v-if="formData.Source==='db'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text">
|
||||
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-personal') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.email.label') }}</label>
|
||||
<input v-model="formData.Email" class="form-control" :placeholder="$t('modals.user-edit.email.placeholder')" type="email">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.firstname.label') }}</label>
|
||||
<input v-model="formData.Firstname" class="form-control" :placeholder="$t('modals.user-edit.firstname.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.lastname.label') }}</label>
|
||||
<input v-model="formData.Lastname" class="form-control" :placeholder="$t('modals.user-edit.lastname.placeholder')" type="text">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.phone.label') }}</label>
|
||||
<input v-model="formData.Phone" class="form-control" :placeholder="$t('modals.user-edit.phone.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.department.label') }}</label>
|
||||
<input v-model="formData.Department" class="form-control" :placeholder="$t('modals.user-edit.department.placeholder')" type="text">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-notes') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.notes.label') }}</label>
|
||||
<textarea v-model="formData.Notes" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-state') }}</legend>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input v-model="formData.Locked" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.locked.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Source==='db'">
|
||||
<input v-model="formData.IsAdmin" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.user-edit.admin.label') }}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex-fill text-start">
|
||||
<button v-if="props.userId!=='#NEW#'&&formData.Source==='db'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@@ -1,143 +0,0 @@
|
||||
<script setup>
|
||||
import Modal from "./Modal.vue";
|
||||
import {userStore} from "../stores/users";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const users = userStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: String,
|
||||
visible: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const selectedUser = computed(() => {
|
||||
let user = users.Find(props.userId)
|
||||
if (user) {
|
||||
return user
|
||||
}
|
||||
|
||||
return {} // return empty object to avoid "undefined" access problems
|
||||
})
|
||||
|
||||
const title = computed(() => {
|
||||
if (!props.visible) {
|
||||
return "" // otherwise interfaces.GetSelected will die...
|
||||
}
|
||||
return t("modals.user-view.headline") + " " + selectedUser.value.Identifier
|
||||
})
|
||||
|
||||
const userPeers = computed(() => {
|
||||
return users.Peers
|
||||
})
|
||||
|
||||
// functions
|
||||
|
||||
watch(() => props.visible, async (newValue, oldValue) => {
|
||||
if (oldValue === false && newValue === true) { // if modal is shown
|
||||
await users.LoadUserPeers(selectedUser.value.Identifier)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#user">{{ $t('modals.user-view.tab-user') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#peers">{{ $t('modals.user-view.tab-peers') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="interfaceTabs" class="tab-content">
|
||||
<div id="user" class="tab-pane fade active show">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<h4>{{ $t('modals.user-view.headline-info') }}</h4>
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.email') }}:</td>
|
||||
<td>{{selectedUser.Email}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.firstname') }}:</td>
|
||||
<td>{{selectedUser.Firstname}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.lastname') }}:</td>
|
||||
<td>{{selectedUser.Lastname}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.phone') }}:</td>
|
||||
<td>{{selectedUser.Phone}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('modals.user-view.department') }}:</td>
|
||||
<td>{{selectedUser.Department}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Disabled">
|
||||
<td>{{ $t('modals.user-view.disabled') }}:</td>
|
||||
<td>{{selectedUser.DisabledReason}}</td>
|
||||
</tr>
|
||||
<tr v-if="selectedUser.Locked">
|
||||
<td>{{ $t('modals.user-view.locked') }}:</td>
|
||||
<td>{{selectedUser.LockedReason}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
<li class="list-group-item" v-if="selectedUser.Notes">
|
||||
<h4>{{ $t('modals.user-view.headline-notes') }}</h4>
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr><td>{{selectedUser.Notes}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="peers" class="tab-pane fade">
|
||||
<ul v-if="userPeers.length===0" class="list-group list-group-flush">
|
||||
<li class="list-group-item">{{ $t('modals.user-view.no-peers') }}</li>
|
||||
</ul>
|
||||
|
||||
<table v-if="userPeers.length!==0" id="peerTable" class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.name') }}</th>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.interface') }}</th>
|
||||
<th scope="col">{{ $t('modals.user-view.peers.ip') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="peer in userPeers" :key="peer.Identifier">
|
||||
<td>{{peer.DisplayName}}</td>
|
||||
<td>{{peer.InterfaceIdentifier}}</td>
|
||||
<td>
|
||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge pill bg-light">{{ ip }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-primary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|