Compare commits
7 Commits
mikrotik-i
...
legacy
Author | SHA1 | Date | |
---|---|---|---|
|
e6b01a9903 | ||
|
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' }}
|
||||
|
22
.github/workflows/pages.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: github-pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ github.ref }}
|
||||
path: .cache
|
||||
- run: pip install mkdocs-material
|
||||
- run: pip install pillow cairosvg
|
||||
- run: mkdocs gh-deploy --force
|
8
.gitignore
vendored
@@ -28,14 +28,12 @@
|
||||
out/
|
||||
dist/
|
||||
data/
|
||||
docker_images/
|
||||
ssh.key
|
||||
.testCoverage.txt
|
||||
wg_portal.db
|
||||
sqlite.db
|
||||
go.sum
|
||||
swagger.json
|
||||
swagger.yaml
|
||||
/config.yml
|
||||
/config/
|
||||
venv/
|
||||
.cache/
|
||||
sqlite.db
|
||||
node_modules/
|
@@ -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>
|
23
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
|
||||
@@ -30,25 +30,24 @@ RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platfo
|
||||
######-
|
||||
# Here starts the main image
|
||||
######-
|
||||
FROM alpine:3.19
|
||||
|
||||
# Install OS-level dependencies
|
||||
RUN apk add --no-cache bash openresolv
|
||||
FROM scratch
|
||||
|
||||
# Setup timezone
|
||||
ENV TZ=Europe/Vienna
|
||||
|
||||
# Import linux stuff from builder.
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
COPY --from=builder /etc/group /etc/group
|
||||
|
||||
# Copy binaries
|
||||
COPY --from=builder /build/dist/wg-portal /app/wg-portal
|
||||
COPY --from=builder /build/dist/hc /app/hc
|
||||
|
||||
# Set the Current Working Directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# by default, the web-portal is reachable on port 8888
|
||||
EXPOSE 8888/tcp
|
||||
|
||||
# the database and config file can be mounted from the host
|
||||
VOLUME [ "/app/data", "/app/config" ]
|
||||
|
||||
# Command to run the executable
|
||||
ENTRYPOINT [ "/app/wg-portal" ]
|
||||
CMD [ "/app/wg-portal" ]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD [ "/app/hc", "http://localhost:11223/health" ]
|
||||
|
@@ -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>
|
286
assets/tpl/admin_index.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Admin</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN Administration</h1>
|
||||
{{template "prt_flashes.html" .}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="mr-auto">Interface status for <strong>{{.Device.DeviceName}}</strong> {{if eq $.Device.Type "server"}}(server mode){{end}}{{if eq $.Device.Type "client"}}(client mode){{end}}</span>
|
||||
<a href="/admin/device/write?dev={{.Device.DeviceName}}" title="Write interface configuration"><i class="fas fa-save"></i></a>
|
||||
|
||||
<a href="/admin/device/download?dev={{.Device.DeviceName}}" title="Download interface configuration"><i class="fas fa-download"></i></a>
|
||||
|
||||
<a href="/admin/device/edit?dev={{.Device.DeviceName}}" title="Edit interface settings"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Endpoint:</td>
|
||||
<td>{{.Device.DefaultEndpoint}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Listening Port:</td>
|
||||
<td>{{.Device.ListenPort}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Peers:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Peers:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default allowed IP's:</td>
|
||||
<td>{{.Device.DefaultAllowedIPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default Keepalive Interval:</td>
|
||||
<td>{{.Device.DefaultPersistentKeepalive}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>{{.Device.PublicKey}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled Endpoints:</td>
|
||||
<td>{{len .Device.Interface.Peers}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Endpoints:</td>
|
||||
<td>{{.TotalPeers}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-sm table-borderless device-status-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>IP Address:</td>
|
||||
<td>{{.Device.IPsStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DNS servers:</td>
|
||||
<td>{{.Device.DNSStr}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default MTU:</td>
|
||||
<td>{{.Device.Mtu}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<h2 class="mt-2">Current VPN Peers</h2>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<h2 class="mt-2">Current VPN Endpoints</h2>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
<a href="/admin/peer/emailall" data-toggle="confirmation" data-title="Send mail to all peers?" title="Send mail to all peers" class="btn btn-light"><i class="fa fa-fw fa-paper-plane"></i></a>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<a href="/admin/peer/createldap" title="Add multiple peers" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-users"></i></a>
|
||||
{{end}}
|
||||
<a href="/admin/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "peers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "peers" "pubKey"}}"></i></a></th>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "peers" "mail"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "peers" "ip"}}"></i></a></th>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<th scope="col"><a href="?sort=endpoint">Endpoint <i class="fa fa-fw {{.Session.GetSortIcon "peers" "endpoint"}}"></i></a></th>
|
||||
{{end}}
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "peers" "handshake"}}"></i></a></th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span title="Online status" class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.Email}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "client"}}
|
||||
<td>{{$p.Endpoint}}</td>
|
||||
{{end}}
|
||||
<td>
|
||||
<span
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title=""
|
||||
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
|
||||
{{$p.LastHandshakeRelativeTime}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{if eq $.Session.IsAdmin true}}
|
||||
<a href="/admin/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="7" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t3{{$p.UID}}">Danger Zone</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Connection / Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-network-wired" title="Last Endpoint"></i> {{$p.Peer.Endpoint}}{{end}}</p>
|
||||
<p class="ml-4">{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down" title="Download"></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up" title="Upload"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="t3{{$p.UID}}" class="tab-pane fade">
|
||||
<a href="/admin/peer/delete?pkey={{$p.PublicKey}}" class="btn btn-danger" title="Delete peer">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<img class="list-image-large" loading="lazy" alt="Configuration QR Code" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Peer will expire on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
{{if eq $.Device.Type "server"}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/admin/peer/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/admin/peer/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
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>
|
145
assets/tpl/user_index.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>{{ .Static.WebsiteTitle }} - Profile</title>
|
||||
<meta name="description" content="{{ .Static.WebsiteTitle }}">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="stylesheet" href="/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
{{template "prt_nav.html" .}}
|
||||
<div class="container mt-5">
|
||||
<h1>WireGuard VPN User-Portal</h1>
|
||||
|
||||
<div class="mt-4 row">
|
||||
<div class="col-sm-8 col-12">
|
||||
<h2 class="mt-2">Your VPN Profiles</h2>
|
||||
</div>
|
||||
<div class="col-sm-4 col-12 text-right">
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<a href="/user/peer/create" title="Add a peer" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i><i class="fa fa-fw fa-user"></i></a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<table class="table table-sm" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="list-image-cell"></th><!-- Status and expand -->
|
||||
<th scope="col"><a href="?sort=id">Identifier <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "id"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=pubKey">Public Key <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "pubKey"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=mail">E-Mail <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "mail"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=ip">IP's <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "ip"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=device">Interface <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "device"}}"></i></a></th>
|
||||
<th scope="col"><a href="?sort=handshake">Handshake <i class="fa fa-fw {{.Session.GetSortIcon "userpeers" "handshake"}}"></i></a></th>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<th scope="col"></th>
|
||||
{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $p :=.Peers}}
|
||||
{{$peerUser:=(userForEmail $.Users $p.Email)}}
|
||||
<tr id="user-pos-{{$i}}" {{if $p.DeactivatedAt}}class="disabled-peer"{{end}}>
|
||||
<th scope="row" class="list-image-cell">
|
||||
<a href="#{{$p.UID}}" data-toggle="collapse" class="collapse-indicator collapsed"></a>
|
||||
<!-- online check -->
|
||||
<span class="online-status" id="online-{{$p.UID}}" data-pkey="{{$p.PublicKey}}"><i class="fas fa-unlink"></i></span>
|
||||
</th>
|
||||
<td>{{$p.Identifier}}{{if $p.WillExpire}} <i class="fas fa-hourglass-end expiring-peer" data-toggle="tooltip" data-placement="right" title="" data-original-title="Expires at: {{formatDate $p.ExpiresAt}}"></i>{{end}}</td>
|
||||
<td>{{$p.PublicKey}}</td>
|
||||
<td>{{$p.Email}}</td>
|
||||
<td>{{$p.IPsStr}}</td>
|
||||
<td>{{$p.DeviceName}}</td>
|
||||
<td>
|
||||
<span
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title=""
|
||||
data-original-title="{{if $p.LastHandshakeTime.IsZero}}Never connected, or user is disabled.{{else}}{{formatTime $p.LastHandshakeTime}}{{end}}">
|
||||
{{$p.LastHandshakeRelativeTime}}
|
||||
</span>
|
||||
</td>
|
||||
{{if eq $.UserManagePeers true}}
|
||||
<td>
|
||||
<a href="/user/peer/edit?pkey={{$p.PublicKey}}" title="Edit peer"><i class="fas fa-cog"></i></a>
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
<tr class="hiddenRow">
|
||||
<td colspan="6" class="hiddenCell" style="white-space:nowrap">
|
||||
<div class="collapse" id="{{$p.UID}}" data-parent="#userTable">
|
||||
<div class="row collapsedRow">
|
||||
<div class="col-md-6 leftBorder">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#t1{{$p.UID}}">Personal</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#t2{{$p.UID}}">Configuration</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="tabContent{{$p.UID}}">
|
||||
<div id="t1{{$p.UID}}" class="tab-pane fade active show">
|
||||
<h4>User details</h4>
|
||||
{{if not $peerUser}}
|
||||
<p>No user information available...</p>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li>Firstname: {{$peerUser.Firstname}}</li>
|
||||
<li>Lastname: {{$peerUser.Lastname}}</li>
|
||||
<li>Phone: {{$peerUser.Phone}}</li>
|
||||
<li>Mail: {{$peerUser.Email}}</li>
|
||||
</ul>
|
||||
{{end}}
|
||||
<h4>Traffic</h4>
|
||||
{{if not $p.Peer}}
|
||||
<p>No Traffic data available...</p>
|
||||
{{else}}
|
||||
<p>{{if $p.DeactivatedAt}}-{{else}}<i class="fas fa-long-arrow-alt-down"></i></i> {{formatBytes $p.Peer.ReceiveBytes}} / <i class="fas fa-long-arrow-alt-up"></i> {{formatBytes $p.Peer.TransmitBytes}}{{end}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="t2{{$p.UID}}" class="tab-pane fade">
|
||||
<pre>{{$p.Config}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<img class="list-image-large" src="/user/qrcode?pkey={{$p.PublicKey}}"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{if $p.DeactivatedAt}}
|
||||
<div class="pull-right-lg mt-lg-5 disabled-peer">Peer is disabled! <i class="fas fa-comment-dots" data-toggle="tooltip" data-placement="left" title="" data-original-title="Reason: {{$p.DeactivatedReason}}"></i></div>
|
||||
{{end}}
|
||||
{{if $p.WillExpire}}
|
||||
<div class="pull-right-lg mt-lg-5 expiring-peer"><i class="fas fa-exclamation-triangle"></i> Profile expires on {{ formatDate $p.ExpiresAt}}</div>
|
||||
{{end}}
|
||||
<div class="pull-right-lg mt-lg-5 mt-md-3">
|
||||
<a href="/user/download?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Download configuration">Download</a>
|
||||
<a href="/user/email?pkey={{$p.PublicKey}}" class="btn btn-primary" title="Send configuration via Email">Email</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Currently listed peers: <strong>{{len .Peers}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.html" .}}
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/bootstrap-confirmation.min.js"></script>
|
||||
<script src="/js/custom.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@@ -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
|
||||
|
@@ -1 +0,0 @@
|
||||
wgportal.org
|
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>WireGuard icon</title><path d="M23.98 11.645S24.533 0 11.735 0C.418 0 .064 11.17.064 11.17S-1.6 24 11.997 24C25.04 24 23.98 11.645 23.98 11.645zM8.155 7.576c2.4-1.47 5.469-.571 6.618 1.638.218.419.246 1.063.108 1.503-.477 1.516-1.601 2.366-3.145 2.728.455-.39.817-.832.933-1.442a2.112 2.112 0 0 0-.364-1.677 2.14 2.14 0 0 0-2.465-.75c-.95.36-1.47 1.228-1.377 2.294.087.99.839 1.632 2.245 1.876-.21.111-.372.193-.53.281a5.113 5.113 0 0 0-1.644 1.43c-.143.192-.24.208-.458.075-2.827-1.729-3.009-6.067.078-7.956zM6.04 18.258c-.455.116-.895.286-1.359.438.227-1.532 2.021-2.943 3.539-2.782a3.91 3.91 0 0 0-.74 2.072c-.504.093-.98.155-1.44.272zM15.703 3.3c.448.017.898.01 1.347.02a2.324 2.324 0 0 1 .334.047 3.249 3.249 0 0 1-.34.434c-.16.15-.341.296-.573.069-.055-.055-.187-.042-.283-.044-.447-.005-.894-.02-1.34-.003a8.323 8.323 0 0 0-1.154.118c-.072.013-.178.25-.146.338.078.207.191.435.359.567.619.49 1.277.928 1.9 1.413.604.472 1.167.99 1.51 1.7.446.928.46 1.9.267 2.877-.322 1.63-1.147 2.98-2.483 3.962-.538.395-1.205.62-1.821.903-.543.25-1.1.465-1.644.712-.98.446-1.53 1.51-1.369 2.615.149 1.015 1.04 1.862 2.059 2.037 1.223.21 2.486-.586 2.785-1.83.336-1.397-.423-2.646-1.845-3.024l-.256-.066c.38-.17.708-.291 1.012-.458q.793-.437 1.558-.925c.15-.096.231-.096.36.014.977.846 1.56 1.898 1.724 3.187.27 2.135-.74 4.096-2.646 5.101-2.948 1.555-6.557-.215-7.208-3.484-.558-2.8 1.418-5.34 3.797-5.83 1.023-.211 1.958-.637 2.685-1.425.47-.508.697-.944.775-1.141a3.165 3.165 0 0 0 .217-1.158 2.71 2.71 0 0 0-.237-.992c-.248-.566-1.2-1.466-1.435-1.656l-2.24-1.754c-.079-.065-.168-.06-.36-.047-.23.016-.815.048-1.067-.018.204-.155.76-.38 1-.56-.726-.49-1.554-.314-2.315-.46.176-.328 1.046-.831 1.541-.888a7.323 7.323 0 0 0-.135-.822c-.03-.111-.154-.22-.263-.283-.262-.154-.541-.281-.843-.434a1.755 1.755 0 0 1 .906-.28 3.385 3.385 0 0 1 .908.088c.54.123.97.042 1.399-.324-.338-.136-.676-.26-1.003-.407a9.843 9.843 0 0 1-.942-.493c.85.118 1.671.437 2.54.32l.022-.118-2.018-.47c1.203-.11 2.323-.128 3.384.388.299.146.61.266.897.432.14.08.233.24.348.365.09.098.164.23.276.29.424.225.89.234 1.366.223l.01-.16c.479.15 1.017.702 1.017 1.105-.776 0-1.55-.003-2.325.004-.083 0-.165.061-.247.094.078.046.155.128.235.131z M14.703 2.153a.118.118 0 0 0-.016.19.179.179 0 0 0 .246.065c.075-.038.148-.078.238-.125-.072-.062-.13-.114-.19-.163-.106-.087-.193-.032-.278.033z"/></svg>
|
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 100 KiB |
@@ -1,11 +0,0 @@
|
||||
To build a standalone application, use the Makefile provided in the repository.
|
||||
Go version **1.21** or higher has to be installed to build WireGuard Portal.
|
||||
If you want to re-compile the frontend, NodeJS **18** and NPM >= **9** is required.
|
||||
|
||||
```shell
|
||||
# build the frontend (optional)
|
||||
make frontend
|
||||
|
||||
# build the binary
|
||||
make build
|
||||
```
|
@@ -1,81 +0,0 @@
|
||||
## Image Usage
|
||||
|
||||
The preferred way to start WireGuard Portal as Docker container is to use Docker Compose.
|
||||
|
||||
A sample docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '3.6'
|
||||
services:
|
||||
wg-portal:
|
||||
image: wgportal/wg-portal:v2
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
network_mode: "host"
|
||||
ports:
|
||||
- "8888:8888"
|
||||
volumes:
|
||||
- /etc/wireguard:/etc/wireguard
|
||||
- ./data:/app/data
|
||||
- ./config:/app/config
|
||||
```
|
||||
|
||||
By default, the webserver is listening on port **8888**.
|
||||
|
||||
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
|
||||
|
||||
## Image Versioning
|
||||
|
||||
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||
There are three types of tags in the repository:
|
||||
|
||||
#### Semantic versioned tags
|
||||
For example, `1.0.19`.
|
||||
|
||||
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
|
||||
|
||||
Once these tags show up in this repository, they will never change.
|
||||
|
||||
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. **wgportal/wg-portal:1.0.19**, instead of the latest or canary tags.
|
||||
|
||||
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v1` or `1.0`.
|
||||
|
||||
Version **1** is currently **stable**, version **2** is in **development**.
|
||||
|
||||
#### latest
|
||||
This is the most recent build to master! It changes a lot and is very unstable.
|
||||
|
||||
We recommend that you don't use it except for development purposes.
|
||||
|
||||
#### Branch tags
|
||||
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
|
||||
|
||||
|
||||
|
||||
## Configuration
|
||||
You can configure WireGuard Portal using a yaml configuration file.
|
||||
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
|
||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
||||
|
||||
By default, WireGuard Portal uses a SQLite database. The database is stored in `/app/data/sqlite.db`.
|
||||
|
||||
You should mount those directories as a volume:
|
||||
- /app/data
|
||||
- /app/config
|
||||
|
||||
### Configuration Options
|
||||
All available YAML configuration options are available [here](https://github.com/h44z/wg-portal#configuration).
|
||||
|
||||
A very basic example:
|
||||
|
||||
```yaml
|
||||
core:
|
||||
admin_user: test@wg-portal.local
|
||||
admin_password: secret
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
request_logging: true
|
||||
```
|
||||
|
@@ -1,25 +0,0 @@
|
||||
For production deployments of WireGuard Portal, we strongly recommend using version 1.
|
||||
If you want to use version 2, please be aware that it is still in beta and not feature complete.
|
||||
|
||||
## Upgrade from v1 to v2
|
||||
|
||||
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
|
||||
|
||||
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
|
||||
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
||||
|
||||
To upgrade from a previous SQLite database, start wg-portal like:
|
||||
|
||||
```shell
|
||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
||||
```
|
||||
|
||||
You can also specify the database type using the parameter **-migrateFromType**, supported types: mysql, mssql, postgres or sqlite.
|
||||
For example:
|
||||
|
||||
```shell
|
||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom=user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
```
|
||||
|
||||
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yml** configuration file.
|
||||
Ensure that the new database does not contain any data!
|
@@ -1,29 +0,0 @@
|
||||
**WireGuard Portal** is a simple, web based configuration portal for [WireGuard](https://wireguard.com).
|
||||
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
||||
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
||||
connections.
|
||||
|
||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
|
||||
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
||||
|
||||
## Features
|
||||
* Self-hosted - the whole application is a single binary
|
||||
* Responsive web UI written in Vue.JS
|
||||
* Automatically select IP from the network pool assigned to client
|
||||
* QR-Code for convenient mobile client configuration
|
||||
* Sent email to client with QR-code and client config
|
||||
* Enable / Disable clients seamlessly
|
||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
||||
* User authentication (database, OAuth or LDAP)
|
||||
* IPv6 ready
|
||||
* Docker ready
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* Peer Expiry Feature
|
||||
* Handle route and DNS settings like wg-quick does
|
||||
* ~~REST API for management and client deployment~~ (coming soon)
|
||||
|
||||
## Quick-Start
|
||||
|
||||
The easiest way to get started is to use the provided [Docker image](./getting-started/docker.md).
|
||||
|
@@ -1,4 +0,0 @@
|
||||
---
|
||||
template: layouts/home.html
|
||||
title: WireGuard Portal
|
||||
---
|
@@ -1,49 +0,0 @@
|
||||
/* This file is used for extra styles that are not part of the theme */
|
||||
|
||||
span.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.em {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border-bottom: 1px solid #e3e8ee;
|
||||
}
|
||||
|
||||
a.field {
|
||||
font-weight: 600;
|
||||
/* color: #3c4257; */
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
span.parent-field {
|
||||
font-weight: 600;
|
||||
color:#a3acb9;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
span.type {
|
||||
color: #8792a2;
|
||||
font-size: .7rem;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
span.version {
|
||||
color: #8792a2;
|
||||
font-size: .7rem;
|
||||
float: right;
|
||||
}
|
||||
|
||||
span.faint {
|
||||
color: #8792a2;
|
||||
}
|
||||
|
||||
.md-social__link svg {
|
||||
fill: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.md-tabs__link {
|
||||
font-size: 0.8rem;
|
||||
}
|
@@ -1,433 +0,0 @@
|
||||
|
||||
{% extends "main.html" %}
|
||||
|
||||
<!-- Render hero under tabs -->
|
||||
{% block tabs %}
|
||||
{{ super() }}
|
||||
|
||||
<!-- Additional styles for landing page -->
|
||||
<style>
|
||||
|
||||
/* Apply box shadow on smaller screens that don't display tabs */
|
||||
@media only screen and (max-width: 1220px) {
|
||||
.md-header {
|
||||
box-shadow: 0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);
|
||||
transition: color 250ms,background-color 250ms,box-shadow 250ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide main content for now */
|
||||
.md-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide table of contents */
|
||||
@media screen and (min-width: 60em) {
|
||||
.md-sidebar--secondary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide navigation */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar--primary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get started button */
|
||||
.md-typeset .md-button--primary {
|
||||
color: var(--md-primary-fg-color);
|
||||
background-color: var(--md-primary-bg-color);
|
||||
border-color: var(--md-primary-bg-color);
|
||||
}
|
||||
.md-typeset .md-button--primary:hover {
|
||||
color: var(--md-primary-bg-color);
|
||||
background-color: var(--md-primary-fg-color);
|
||||
border-color: var(--md-primary-bg-color);
|
||||
}
|
||||
|
||||
.tx-hero {
|
||||
max-width: 700px;
|
||||
display: flex;
|
||||
padding: .4rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
.tx-hero h1 {
|
||||
font-weight: 700;
|
||||
font-size: 38px;
|
||||
line-height: 46px;
|
||||
color: rgb(38, 38, 38);
|
||||
}
|
||||
.tx-hero p {
|
||||
color: rgb(92, 92, 92);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.tx-hero__image {
|
||||
max-width: 1000px;
|
||||
min-width: 600px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.tx-hero__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Secondary content styles */
|
||||
.secondary-section {
|
||||
background: rgb(245, 245, 245) none repeat scroll 0% 0%;
|
||||
border-top: 1px solid rgb(222, 222, 222);
|
||||
border-bottom: 1px solid rgb(222, 222, 222)
|
||||
}
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0px 40px;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 30px;
|
||||
letter-spacing: normal;
|
||||
padding: 88px 0px 116px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section.follow {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
|
||||
.secondary-section .g .section .component-wrapper {
|
||||
display: flex;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper h3 {
|
||||
color: rgb(38, 38, 38);
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 46px;
|
||||
letter-spacing: normal;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper h4 {
|
||||
color: rgb(38, 38, 38);
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper p {
|
||||
color: rgb(92, 92, 92);
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 30px;
|
||||
letter-spacing: normal;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .image-wrapper {
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
margin-top: 48px;
|
||||
border: 1px solid rgb(222, 222, 222);
|
||||
box-shadow: rgba(202, 202, 202, 0.15) 0px 0px 0px 6px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .first-column {
|
||||
padding-right: 100px;
|
||||
flex: 0 1 auto;
|
||||
height: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper .first-column {
|
||||
padding-right: 0px;
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .second-column {
|
||||
flex: 0 1 auto;
|
||||
height: auto;
|
||||
width: 50%;
|
||||
}
|
||||
@media screen and (max-width: 1012px) {
|
||||
.secondary-section .g .section .component-wrapper .second-column {
|
||||
width: 100%;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
@media screen and (min-width: 64rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid a.card-wrapper {
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
position: relative;
|
||||
background-color: #fff none repeat scroll 0% 0%;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-moz-box-align: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
-moz-box-pack: start;
|
||||
justify-content: flex-start;
|
||||
box-shadow: rgba(0, 0, 0, 0.09) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.15) 0px 0.25rem 0.5rem 0px;
|
||||
transition: all 0.6s cubic-bezier(0.165, 0.84, 0.44, 1) 0s;
|
||||
}
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card:hover {
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0.3125rem 0.3125rem 0px -0.0625rem, rgba(0, 0, 0, 0.26) 0px 0.25rem 0.5rem 0px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 75rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
padding: 2rem 2.5rem;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 36rem) {
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .logo {
|
||||
margin-right: 0.75rem;
|
||||
width: 1.2rem;
|
||||
min-width: 1.2rem;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content h5 {
|
||||
color: rgb(61, 61, 61);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0;
|
||||
color: rgb(92, 92, 92);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 300;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card .card-content code {
|
||||
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.component-wrapper span.em {
|
||||
color: rgb(61, 61, 61);
|
||||
}
|
||||
|
||||
.component-wrapper a {
|
||||
transition: color 125ms;
|
||||
color: rgb(61, 61, 61);
|
||||
background: rgba(0, 0, 0, 0.05) none repeat scroll 0% 0%;
|
||||
padding: 2px 6px;
|
||||
margin: 0px 1px;
|
||||
border-radius: 4px;
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.component-wrapper a:hover {
|
||||
color: var(--md-typeset-a-color);
|
||||
background: var(--md-accent-fg-color--transparent);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- Hero for landing page -->
|
||||
<div class="md-container tx-hero">
|
||||
<div class="md-grid md-typeset">
|
||||
<div class="md-main__inner">
|
||||
<div>
|
||||
<h1>A beautiful and simple UI to manage your WireGuard peers and interfaces</h1>
|
||||
<p>WireGuard Portal is an open source web-based user interface that makes it easy to setup and manage
|
||||
WireGuard VPN connections. It's built on top of WireGuard's official <span class="em">wgctrl</span> library.</p>
|
||||
</p>
|
||||
<a
|
||||
href="documentation/overview/"
|
||||
title="Get Started"
|
||||
class="md-button md-button--primary"
|
||||
>
|
||||
Get started
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" style="margin-left:2px"><path d="M1 5.16772H9.5M9.5 5.16772L6.5 1.66772M9.5 5.16772L6.5 8.66772" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-container">
|
||||
<div class="tx-hero__image">
|
||||
<img
|
||||
src="{{config.site_url}}assets/images/screenshot.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="md-container secondary-section">
|
||||
<div class="g">
|
||||
<!-- Architecture as building blocks -->
|
||||
<div class="section">
|
||||
<div class="component-wrapper">
|
||||
<div class="first-column">
|
||||
<h3>More information about WireGuard</h3>
|
||||
<p>
|
||||
WireGuard® is an extremely <span class="em">simple</span> yet <span class="em">fast</span> and modern
|
||||
VPN that utilizes <span class="em">state-of-the-art cryptography</span>.
|
||||
</p>
|
||||
<p>
|
||||
WireGuard uses state-of-the-art <a href="https://www.wireguard.com/protocol/">cryptography</a> and still
|
||||
manages to be as easy to configure and deploy as SSH.
|
||||
A combination of extremely high-speed cryptographic primitives and the fact that WireGuard lives inside
|
||||
the Linux kernel means that secure networking can be very high-speed.
|
||||
It is suitable for both small embedded devices like smartphones and fully loaded backbone routers.
|
||||
</p>
|
||||
</div>
|
||||
<div class="second-column">
|
||||
<div class="image-wrapper">
|
||||
<img
|
||||
src="{{config.site_url}}assets/images/wg-tool.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="component-wrapper" style="display: block;">
|
||||
<h4>Explore official documentation</h4>
|
||||
|
||||
<!-- Arch as code -->
|
||||
<div class="responsive-grid">
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/octicons/file-code-24.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Official Website</h5>
|
||||
<p>
|
||||
If you'd like a general conceptual overview of what WireGuard is about, read onward here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Networking -->
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/protocol/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/fontawesome/solid/network-wired.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Protocol & Cryptography</h5>
|
||||
<p>
|
||||
WireGuard uses state-of-the-art cryptography, like the Noise protocol framework, Curve25519, ChaCha20, Poly1305, BLAKE2, SipHash24, HKDF, and secure trusted constructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Customize -->
|
||||
<a class="card-wrapper" href="https://www.wireguard.com/install/">
|
||||
<div class="card">
|
||||
<div class="logo">
|
||||
<span class="twemoji">
|
||||
{% include ".icons/fontawesome/solid/puzzle-piece.svg" %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h5>Client Installation</h5>
|
||||
<p>
|
||||
You may progress to installation and reading the quickstart instructions on how to use it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Content -->
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- Application footer -->
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
@@ -1,17 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{% if page and page.meta and page.meta.title %}
|
||||
<meta property="og:title" content="{{ page.meta.title }}" />
|
||||
{% endif %}
|
||||
{% if page and page.meta and page.meta.image %}
|
||||
<meta property="og:image" content="{{ page.meta.image }}" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="{{ page.meta.image_width }}" />
|
||||
<meta property="og:image:height" content="{{ page.meta.image_height }}" />
|
||||
<meta property="twitter:card" content="summary" />
|
||||
<meta property="twitter:title" content="{{ page.meta.twitter_title }}" />
|
||||
<meta property="twitter:image" content="{{ page.meta.image }}" />
|
||||
<meta property="twitter:image:alt" content="{{ page.meta.image_alt }}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -1,32 +0,0 @@
|
||||
{% import "partials/language.html" as lang with context %}
|
||||
|
||||
<!-- Application footer -->
|
||||
<footer class="md-footer">
|
||||
<!-- Further information -->
|
||||
<div class="md-footer-meta md-typeset" style="background-color: #fff;">
|
||||
<div class="md-footer-meta__inner md-grid" style="background-color: #fff;">
|
||||
|
||||
<!-- Copyright and theme information -->
|
||||
<div class="md-footer-copyright">
|
||||
{% if config.copyright %}
|
||||
<div class="md-footer-copyright__highlight" style="color: rgb(38, 38, 38);">
|
||||
{{ config.copyright }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div style="color: rgb(38, 38, 38);">
|
||||
Made with
|
||||
<a
|
||||
href="https://squidfunk.github.io/mkdocs-material/"
|
||||
target="_blank" rel="noopener"
|
||||
style="color: black;"
|
||||
>
|
||||
Material for MkDocs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social links -->
|
||||
{% include "partials/social.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
12
efs.go
Normal file
@@ -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>
|
1278
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.5.1",
|
||||
"@kyvg/vue3-notification": "^3.1.3",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootswatch": "^5.3.2",
|
||||
"flag-icons": "^7.1.0",
|
||||
"is-cidr": "^5.0.3",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"prismjs": "^1.29.0",
|
||||
"vue": "^3.3.13",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue3-tags-input": "^1.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 18 KiB |