Compare commits
7 Commits
v2.0.0-alp
...
legacy
Author | SHA1 | Date | |
---|---|---|---|
|
e6b01a9903 | ||
|
2f79dd04c0 | ||
|
e5ed9736b3 | ||
|
c8353b85ae | ||
|
6142031387 | ||
|
dd86d0ff49 | ||
|
bdd426a679 |
@@ -7,10 +7,6 @@ jobs:
|
|||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- go-mod-latest-v4-{{ checksum "go.sum" }}
|
- go-mod-latest-v4-{{ checksum "go.sum" }}
|
||||||
- run:
|
|
||||||
name: Build Frontend
|
|
||||||
command: |
|
|
||||||
make frontend
|
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: |
|
command: |
|
||||||
@@ -55,7 +51,30 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
working_directory: ~/repo
|
working_directory: ~/repo
|
||||||
docker:
|
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:
|
workflows:
|
||||||
build-and-release:
|
build-and-release:
|
||||||
@@ -65,3 +84,9 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /^v.*/
|
only: /^v.*/
|
||||||
|
- build-118:
|
||||||
|
requires:
|
||||||
|
- build-latest
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /^v.*/
|
@@ -1,5 +0,0 @@
|
|||||||
.github/
|
|
||||||
**/.vscode/
|
|
||||||
docs/
|
|
||||||
frontend/node_modules/
|
|
||||||
internal/app/api/core/frontend-dist
|
|
20
.github/dependabot.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
# To get started with Dependabot version updates, you'll need to specify which
|
|
||||||
# package ecosystems to update and where the package manifests are located.
|
|
||||||
# Please see the documentation for all configuration options:
|
|
||||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
|
||||||
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
- package-ecosystem: gomod
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: weekly
|
|
13
.github/workflows/codeql-analysis.yml
vendored
@@ -24,25 +24,22 @@ jobs:
|
|||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
# required for all workflows
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'go', 'javascript-typescript' ]
|
language: [ 'go', 'javascript' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -53,7 +50,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -67,4 +64,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v1
|
||||||
|
136
.github/workflows/docker-publish.yml
vendored
@@ -1,79 +1,139 @@
|
|||||||
name: Docker
|
name: Docker
|
||||||
|
|
||||||
on:
|
# This workflow uses actions that are not certified by GitHub.
|
||||||
pull_request:
|
# They are provided by a third-party and are governed by
|
||||||
branches: [master]
|
# separate terms of service, privacy policy, and support
|
||||||
push:
|
# documentation.
|
||||||
branches: [master, stable]
|
|
||||||
# Publish vX.X.X tags as releases.
|
on:
|
||||||
tags: ["v*.*.*"]
|
push:
|
||||||
|
branches: [ master, stable ]
|
||||||
|
# Publish vX.X.X tags as releases.
|
||||||
|
tags: [ 'v*.*.*' ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Use docker.io for Docker Hub if empty
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
# github.repository as <account>/<repo>
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-n-push:
|
build-dockerhub:
|
||||||
name: Build and Push
|
name: Push Docker image to Docker Hub
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Get Version
|
- name: Get Version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: echo "BUILD_VERSION=${GITHUB_REF_NAME}-${GITHUB_SHA::7}" >> $GITHUB_ENV
|
run: |
|
||||||
|
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
|
||||||
|
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
|
||||||
|
id: get_version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: wgportal/wg-portal
|
||||||
wgportal/wg-portal
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=auto
|
latest=false
|
||||||
prefix=
|
prefix=
|
||||||
suffix=
|
suffix=
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern=v{{major}}
|
type=semver,pattern=v{{major}}
|
||||||
# set latest tag for default branch
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
annotations: ${{ steps.meta.outputs.annotations }}
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
build-args: |
|
build-args: |
|
||||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
|
||||||
|
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
|
||||||
|
|
||||||
|
build-github:
|
||||||
|
name: Push Docker image to Github Container Registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Get Version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=identifier::$(echo ${GITHUB_REF##*/})"
|
||||||
|
echo "::set-output name=hash::$(echo ${GITHUB_SHA} | cut -c1-7)"
|
||||||
|
id: get_version
|
||||||
|
|
||||||
|
# Login against a Docker registry except on PR
|
||||||
|
# https://github.com/docker/login-action
|
||||||
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Extract metadata (tags, labels) for Docker
|
||||||
|
# https://github.com/docker/metadata-action
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
|
prefix=
|
||||||
|
suffix=
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern=v{{major}}
|
||||||
|
|
||||||
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
|
# https://github.com/docker/build-push-action
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
build-args: |
|
||||||
|
BUILD_IDENTIFIER=${{ steps.get_version.outputs.identifier }}
|
||||||
|
BUILD_VERSION=${{ steps.get_version.outputs.hash }}
|
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@v5
|
|
||||||
with:
|
|
||||||
python-version: 3.x
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
key: ${{ github.ref }}
|
|
||||||
path: .cache
|
|
||||||
- run: pip install mkdocs-material
|
|
||||||
- run: pip install pillow cairosvg
|
|
||||||
- run: mkdocs gh-deploy --force
|
|
9
.gitignore
vendored
@@ -28,15 +28,12 @@
|
|||||||
out/
|
out/
|
||||||
dist/
|
dist/
|
||||||
data/
|
data/
|
||||||
|
docker_images/
|
||||||
ssh.key
|
ssh.key
|
||||||
.testCoverage.txt
|
.testCoverage.txt
|
||||||
wg_portal.db
|
wg_portal.db
|
||||||
sqlite.db
|
|
||||||
swagger.json
|
swagger.json
|
||||||
swagger.yaml
|
swagger.yaml
|
||||||
/config.yml
|
/config.yml
|
||||||
/config/
|
sqlite.db
|
||||||
venv/
|
node_modules/
|
||||||
.cache/
|
|
||||||
# ignore local frontend dist directory
|
|
||||||
internal/app/api/core/frontend-dist
|
|
@@ -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>
|
|
87
Dockerfile
@@ -1,60 +1,53 @@
|
|||||||
# Dockerfile References: https://docs.docker.com/engine/reference/builder/
|
# Dockerfile References: https://docs.docker.com/engine/reference/builder/
|
||||||
# This dockerfile uses a multi-stage build system to reduce the image footprint.
|
# This dockerfile uses a multi-stage build system to reduce the image footprint.
|
||||||
|
|
||||||
######
|
######-
|
||||||
# Build frontend
|
# Start from the latest golang base image as builder image (only used to compile the code)
|
||||||
######
|
######-
|
||||||
FROM --platform=${BUILDPLATFORM} node:lts-alpine as frontend
|
FROM golang:1.18 as builder
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /build
|
ARG BUILD_IDENTIFIER
|
||||||
# Download dependencies
|
ENV ENV_BUILD_IDENTIFIER=$BUILD_IDENTIFIER
|
||||||
COPY frontend/package.json frontend/package-lock.json ./
|
|
||||||
RUN npm ci
|
|
||||||
# Set dist output directory
|
|
||||||
ENV DIST_OUT_DIR="dist"
|
|
||||||
# Copy the sources to the working directory
|
|
||||||
COPY frontend .
|
|
||||||
# Build the frontend
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
######
|
|
||||||
# Build backend
|
|
||||||
######
|
|
||||||
FROM --platform=${BUILDPLATFORM} golang:1.21-alpine as builder
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /build
|
|
||||||
# Download dependencies
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
# Copy the sources to the working directory
|
|
||||||
COPY . .
|
|
||||||
# Copy the frontend build result
|
|
||||||
COPY --from=frontend /build/dist/ ./internal/app/api/core/frontend-dist/
|
|
||||||
# Set the build version from arguments
|
|
||||||
ARG BUILD_VERSION
|
ARG BUILD_VERSION
|
||||||
# Split to cross-platform build
|
ENV ENV_BUILD_VERSION=$BUILD_VERSION
|
||||||
ARG TARGETARCH
|
|
||||||
# Build the application
|
# populated by BuildKit
|
||||||
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} go build -o /build/dist/wg-portal \
|
ARG TARGETPLATFORM
|
||||||
-ldflags "-w -s -extldflags '-static' -X 'github.com/h44z/wg-portal/internal.Version=${BUILD_VERSION}'" \
|
ENV ENV_TARGETPLATFORM=$TARGETPLATFORM
|
||||||
-tags netgo \
|
|
||||||
cmd/wg-portal/main.go
|
RUN mkdir /build
|
||||||
|
|
||||||
|
# Copy the source from the current directory to the Working Directory inside the container
|
||||||
|
ADD . /build/
|
||||||
|
|
||||||
|
# Set the Current Working Directory inside the container
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build the Go app
|
||||||
|
RUN echo "Building version '$ENV_BUILD_IDENTIFIER-$ENV_BUILD_VERSION' for platform $ENV_TARGETPLATFORM"; make build
|
||||||
|
|
||||||
|
######-
|
||||||
|
# Here starts the main image
|
||||||
|
######-
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
######
|
|
||||||
# Final image
|
|
||||||
######
|
|
||||||
FROM alpine:3.19
|
|
||||||
# Install OS-level dependencies
|
|
||||||
RUN apk add --no-cache bash curl iptables nftables openresolv
|
|
||||||
# Setup timezone
|
# Setup timezone
|
||||||
ENV TZ=Europe/Vienna
|
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 binaries
|
||||||
COPY --from=builder /build/dist/wg-portal /app/wg-portal
|
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
|
# Set the Current Working Directory inside the container
|
||||||
WORKDIR /app
|
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
|
# 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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
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
|
BUILDDIR=dist
|
||||||
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
|
BINARIES=$(subst cmd/,,$(wildcard cmd/*))
|
||||||
IMAGE=h44z/wg-portal
|
IMAGE=h44z/wg-portal
|
||||||
NPMCMD=npm
|
|
||||||
|
|
||||||
all: help
|
all: help
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ help:
|
|||||||
#> codegen: Re-generate autogenerated files (like API docs)
|
#> codegen: Re-generate autogenerated files (like API docs)
|
||||||
.PHONY: codegen
|
.PHONY: codegen
|
||||||
codegen: $(SUBDIRS)
|
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/
|
cd internal; swag init --propertyStrategy pascalcase --parseInternal --generalInfo server/api.go --output server/docs/
|
||||||
$(GOCMD) fmt internal/server/docs/docs.go
|
$(GOCMD) fmt internal/server/docs/docs.go
|
||||||
|
|
||||||
@@ -75,56 +75,55 @@ clean:
|
|||||||
#< build: Build all executables (architecture depends on build system)
|
#< build: Build all executables (architecture depends on build system)
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: build-dependencies
|
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}'" \
|
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||||
-tags netgo \
|
-tags netgo \
|
||||||
cmd/wg-portal/main.go
|
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
|
#< build-amd64: Build all executables for AMD64
|
||||||
.PHONY: build-amd64
|
.PHONY: build-amd64
|
||||||
build-amd64: build-dependencies
|
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}'" \
|
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||||
-tags netgo \
|
-tags netgo \
|
||||||
cmd/wg-portal/main.go
|
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
|
#< build-arm64: Build all executables for ARM64
|
||||||
.PHONY: build-arm64
|
.PHONY: build-arm64
|
||||||
build-arm64: build-dependencies
|
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}'" \
|
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||||
-tags netgo \
|
-tags netgo \
|
||||||
cmd/wg-portal/main.go
|
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
|
#< build-arm: Build all executables for ARM32
|
||||||
.PHONY: build-arm
|
.PHONY: build-arm
|
||||||
build-arm: build-dependencies
|
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}'" \
|
-ldflags "-w -s -extldflags \"-static\" -X 'github.com/h44z/wg-portal/internal/server.Version=${ENV_BUILD_IDENTIFIER}-${ENV_BUILD_VERSION}'" \
|
||||||
-tags netgo \
|
-tags netgo \
|
||||||
cmd/wg-portal/main.go
|
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
|
#< build-dependencies: Generate the output directory for compiled executables and download dependencies
|
||||||
.PHONY: build-dependencies
|
.PHONY: build-dependencies
|
||||||
build-dependencies:
|
build-dependencies:
|
||||||
@$(GOCMD) mod download -x
|
@$(GOCMD) mod download -x
|
||||||
@mkdir -p $(BUILDDIR)
|
@mkdir -p $(BUILDDIR)
|
||||||
cp scripts/wg-portal.service $(BUILDDIR)
|
cp scripts/wg-portal.service $(BUILDDIR)
|
||||||
|
cp scripts/wg-portal.env $(BUILDDIR)
|
||||||
#< frontend: Build Vue.js frontend
|
|
||||||
frontend: frontend-dependencies
|
|
||||||
cd frontend; $(NPMCMD) run build
|
|
||||||
|
|
||||||
#< frontend-dependencies: Generate the output directory for compiled executables and download frontend dependencies
|
|
||||||
.PHONY: frontend-dependencies
|
|
||||||
frontend-dependencies:
|
|
||||||
@mkdir -p $(BUILDDIR)
|
|
||||||
cd frontend; $(NPMCMD) install
|
|
||||||
|
|
||||||
#< build-docker: Build a docker image on the current host system
|
|
||||||
.PHONY: build-docker
|
|
||||||
build-docker:
|
|
||||||
docker build --progress=plain \
|
|
||||||
--build-arg BUILD_IDENTIFIER=${ENV_BUILD_IDENTIFIER} --build-arg BUILD_VERSION=${ENV_BUILD_VERSION} \
|
|
||||||
--build-arg TARGETPLATFORM=unknown . \
|
|
||||||
-t h44z/wg-portal:local
|
|
||||||
|
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`
|
379
README.md
@@ -1,4 +1,4 @@
|
|||||||
# WireGuard Portal (v2 - testing)
|
# WireGuard Portal (v1)
|
||||||
|
|
||||||
[](https://travis-ci.com/h44z/wg-portal)
|
[](https://travis-ci.com/h44z/wg-portal)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
@@ -6,204 +6,255 @@
|
|||||||
[](https://goreportcard.com/report/github.com/h44z/wg-portal)
|
[](https://goreportcard.com/report/github.com/h44z/wg-portal)
|
||||||

|

|
||||||

|

|
||||||
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
[](https://hub.docker.com/r/h44z/wg-portal/)
|
||||||
|
|
||||||
> :warning: **IMPORTANT** Version 2 is currently under development and may contain bugs. It is currently not advised to use this version
|
|
||||||
in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
|
||||||
|
|
||||||
Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to: https://hub.docker.com/r/wgportal/wg-portal.
|
|
||||||
Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
|
||||||
|
|
||||||
A simple, web based configuration portal for [WireGuard](https://wireguard.com).
|
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
|
The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) library to manage existing VPN
|
||||||
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
interfaces. This allows for seamless activation or deactivation of new users, without disturbing existing VPN
|
||||||
connections.
|
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
|
## Features
|
||||||
* Self-hosted - the whole application is a single binary
|
* Self-hosted and web based
|
||||||
* Responsive web UI written in Vue.JS
|
|
||||||
* Automatically select IP from the network pool assigned to client
|
* Automatically select IP from the network pool assigned to client
|
||||||
* QR-Code for convenient mobile client configuration
|
* QR-Code for convenient mobile client configuration
|
||||||
* Sent email to client with QR-code and client config
|
* Sent email to client with QR-code and client config
|
||||||
* Enable / Disable clients seamlessly
|
* Enable / Disable clients seamlessly
|
||||||
* Generation of wg-quick configuration file (`wgX.conf`) if required
|
* Generation of `wgX.conf` after any modification
|
||||||
* User authentication (database, OAuth or LDAP)
|
|
||||||
* IPv6 ready
|
* IPv6 ready
|
||||||
* Docker ready
|
* User authentication (SQLite/MySQL and LDAP)
|
||||||
|
* Dockerized
|
||||||
|
* Responsive template
|
||||||
|
* One single binary
|
||||||
* Can be used with existing WireGuard setups
|
* Can be used with existing WireGuard setups
|
||||||
* Support for multiple WireGuard interfaces
|
* Support for multiple WireGuard interfaces
|
||||||
|
* REST API for management and client deployment
|
||||||
* Peer Expiry Feature
|
* 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
|
## Configuration
|
||||||
You can configure WireGuard Portal using a yaml configuration file.
|
You can configure WireGuard Portal using either environment variables or a yaml configuration file.
|
||||||
The filepath of the yaml configuration file defaults to **config/config.yml** in the working directory of the executable.
|
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 **WG_PORTAL_CONFIG**.
|
It is possible to override the configuration filepath using the environment variable **CONFIG_FILE**.
|
||||||
For example: `WG_PORTAL_CONFIG=/home/test/config.yml ./wg-portal-amd64`.
|
For example: `CONFIG_FILE=/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.
|
|
||||||
|
|
||||||
### Configuration Options
|
### Configuration Options
|
||||||
The following configuration options are available:
|
The following configuration options are available:
|
||||||
|
|
||||||
| configuration key | parent key | default_value | description |
|
| environment | yaml | yaml_parent | 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. |
|
| 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. |
|
||||||
| admin_password | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
| 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. |
|
||||||
| editable_keys | core | true | Allow to edit key-pairs in the UI. |
|
| WEBSITE_TITLE | title | core | WireGuard VPN | The website title. |
|
||||||
| create_default_peer | core | false | If an LDAP user logs in for the first time and has no peers associated, a new WireGuard peer will be created for all server interfaces. |
|
| COMPANY_NAME | company | core | WireGuard Portal | The company name (for branding). |
|
||||||
| create_default_peer_on_creation | core | false | If an LDAP user is created (e.g. through LDAP sync), a new WireGuard peer will be created for all server interfaces. |
|
| MAIL_FROM | mailFrom | core | WireGuard VPN <noreply@company.com> | The email address from which emails are sent. |
|
||||||
| self_provisioning_allowed | core | false | Allow registered users to automatically create peers via their profile page. |
|
| LOGO_URL | logoUrl | core | /img/header-logo.png | The logo displayed in the page's header. |
|
||||||
| import_existing | core | true | Import existing WireGuard interfaces and peers into WireGuard Portal. |
|
| ADMIN_USER | adminUser | core | admin@wgportal.local | The administrator user. Must be a valid email address. |
|
||||||
| restore_state | core | true | Restore the WireGuard interface state after WireGuard Portal has started. |
|
| ADMIN_PASS | adminPass | core | wgportal | The administrator password. If unchanged, a random password will be set on first startup. |
|
||||||
| log_level | advanced | warn | The loglevel, can be one of: trace, debug, info, warn, error. |
|
| EDITABLE_KEYS | editableKeys | core | true | Allow to edit key-pairs in the UI. |
|
||||||
| log_pretty | advanced | false | Uses pretty, colorized log messages. |
|
| 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. |
|
||||||
| log_json | advanced | false | Logs in JSON format. |
|
| SELF_PROVISIONING | selfProvisioning | core | false | Allow registered users to automatically create peers via the RESTful API. |
|
||||||
| ldap_sync_interval | advanced | 15m | The time interval after which users will be synchronized from LDAP. |
|
| WG_EXPORTER_FRIENDLY_NAMES | wgExporterFriendlyNames | core | false | Enable integration with [prometheus_wireguard_exporter friendly name](https://github.com/MindFlavor/prometheus_wireguard_exporter#friendly-tags). |
|
||||||
| start_listen_port | advanced | 51820 | The first port number that will be used as listening port for new interfaces. |
|
| LDAP_ENABLED | ldapEnabled | core | false | Enable or disable the LDAP backend. |
|
||||||
| start_cidr_v4 | advanced | 10.11.12.0/24 | The first IPv4 subnet that will be used for new interfaces. |
|
| SESSION_SECRET | sessionSecret | core | secret | Use a custom secret to encrypt session data. |
|
||||||
| start_cidr_v6 | advanced | fdfd:d3ad:c0de:1234::0/64 | The first IPv6 subnet that will be used for new interfaces. |
|
| BACKGROUND_TASK_INTERVAL | backgroundTaskInterval | core | 900 | The interval (in seconds) for the background tasks (like peer expiry check). |
|
||||||
| use_ip_v6 | advanced | true | Enable IPv6 support. |
|
| EXPIRY_REENABLE | expiryReEnable | core | false | Reactivate expired peers if the expiration date is in the future. |
|
||||||
| config_storage_path | advanced | | If a wg-quick style configuration should be stored to the filesystem, specify a storage directory. |
|
| DATABASE_TYPE | typ | database | sqlite | Either mysql or sqlite. |
|
||||||
| expiry_check_interval | advanced | 15m | The interval after which existing peers will be checked if they expired. |
|
| DATABASE_HOST | host | database | | The mysql server address. |
|
||||||
| rule_prio_offset | advanced | 20000 | The default offset for ip route rule priorities. |
|
| DATABASE_PORT | port | database | | The mysql server port. |
|
||||||
| route_table_offset | advanced | 20000 | The default offset for ip route table id's. |
|
| DATABASE_NAME | database | database | data/wg_portal.db | For sqlite database: the database file-path, otherwise the database name. |
|
||||||
| use_ping_checks | statistics | true | If enabled, peers will be pinged periodically to check if they are still connected. |
|
| DATABASE_USERNAME | user | database | | The mysql user. |
|
||||||
| ping_check_workers | statistics | 10 | Number of parallel ping checks that will be executed. |
|
| DATABASE_PASSWORD | password | database | | The mysql password. |
|
||||||
| ping_unprivileged | statistics | false | If set to false, the ping checks will run without root permissions (BETA). |
|
| EMAIL_HOST | host | email | 127.0.0.1 | The email server address. |
|
||||||
| ping_check_interval | statistics | 1m | The interval time between two ping check runs. |
|
| EMAIL_PORT | port | email | 25 | The email server port. |
|
||||||
| data_collection_interval | statistics | 10m | The interval between the data collection cycles. |
|
| EMAIL_TLS | tls | email | false | Use STARTTLS. DEPRECATED: use EMAIL_ENCRYPTION instead. |
|
||||||
| collect_interface_data | statistics | true | A flag to enable interface data collection like bytes sent and received. |
|
| EMAIL_ENCRYPTION | encryption | email | none | Either none, tls or starttls. |
|
||||||
| collect_peer_data | statistics | true | A flag to enable peer data collection like bytes sent and received, last handshake and remote endpoint address. |
|
| EMAIL_CERT_VALIDATION | certcheck | email | false | Validate the email server certificate. |
|
||||||
| collect_audit_data | statistics | true | If enabled, some events, like portal logins, will be logged to the database. |
|
| EMAIL_USERNAME | user | email | | An optional username for SMTP authentication. |
|
||||||
| host | mail | 127.0.0.1 | The mail-server address. |
|
| EMAIL_PASSWORD | pass | email | | An optional password for SMTP authentication. |
|
||||||
| port | mail | 25 | The mail-server SMTP port. |
|
| EMAIL_AUTHTYPE | auth | email | plain | Either plain, login or crammd5. If username and password are empty, this value is ignored. |
|
||||||
| encryption | mail | none | SMTP encryption type, allowed values: none, tls, starttls. |
|
| WG_DEVICES | devices | wg | wg0 | A comma separated list of WireGuard devices. |
|
||||||
| cert_validation | mail | false | Validate the mail server certificate (if encryption tls is used). |
|
| WG_DEFAULT_DEVICE | defaultDevice | wg | wg0 | This device is used for auto-created peers (if CREATE_DEFAULT_PEER is enabled). |
|
||||||
| username | mail | | The SMTP user name. |
|
| WG_CONFIG_PATH | configDirectory | wg | /etc/wireguard | If set, interface configuration updates will be written to this path, filename: <devicename>.conf. |
|
||||||
| password | mail | | The SMTP password. |
|
| MANAGE_IPS | manageIPAddresses | wg | true | Handle IP address setup of interface, only available on linux. |
|
||||||
| auth_type | mail | plain | SMTP authentication type, allowed values: plain, login, crammd5. |
|
| USER_MANAGE_PEERS | userManagePeers | wg | false | Logged in user can create or update peers (partially). |
|
||||||
| from | mail | Wireguard Portal <noreply@wireguard.local> | The address that is used to send mails. |
|
| LDAP_URL | url | ldap | ldap://srv-ad01.company.local:389 | The LDAP server url. |
|
||||||
| link_only | mail | false | Only send links to WireGuard Portal instead of the full configuration. |
|
| LDAP_STARTTLS | startTLS | ldap | true | Use STARTTLS. |
|
||||||
| 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 |
|
| LDAP_CERT_VALIDATION | certcheck | ldap | false | Validate the LDAP server certificate. |
|
||||||
| oidc | auth | Empty Array - no providers configured | A list of OpenID Connect providers. See auth/oidc properties to setup a new provider. |
|
| LDAP_BASEDN | dn | ldap | DC=COMPANY,DC=LOCAL | The base DN for searching users. |
|
||||||
| oauth | auth | Empty Array - no providers configured | A list of plain OAuth providers. See auth/oauth properties to setup a new provider. |
|
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
|
||||||
| ldap | auth | Empty Array - no providers configured | A list of LDAP providers. See auth/ldap properties to setup a new provider. |
|
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. |
|
||||||
| provider_name | auth/oidc | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
| 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. |
|
||||||
| display_name | auth/oidc | | The display name is shown at the login page (the login button). |
|
| 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. |
|
||||||
| base_url | auth/oidc | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
| 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. |
|
||||||
| client_id | auth/oidc | | The OAuth client id. |
|
| LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. |
|
||||||
| client_secret | auth/oidc | | The OAuth client secret. |
|
| LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. |
|
||||||
| extra_scopes | auth/oidc | | Extra scopes that should be used in the OpenID Connect authentication flow. |
|
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |
|
||||||
| field_map | auth/oidc | | Mapping of user fields. Internal fields: user_identifier, email, firstname, lastname, phone, department and is_admin. |
|
| LDAP_ATTR_LASTNAME | attrLastname | ldap | sn | User lastname attribute. |
|
||||||
| registration_enabled | auth/oidc | | If registration is enabled, new user accounts will created in WireGuard Portal. |
|
| LDAP_ATTR_PHONE | attrPhone | ldap | telephoneNumber | User phone number attribute. |
|
||||||
| provider_name | auth/oauth | | A unique provider name. This name must be unique throughout all authentication providers (even other types). |
|
| LDAP_ATTR_GROUPS | attrGroups | ldap | memberOf | User groups attribute. |
|
||||||
| display_name | auth/oauth | | The display name is shown at the login page (the login button). |
|
| LDAP_CERT_CONN | ldapCertConn | ldap | false | Allow connection with certificate against LDAP server without user/password |
|
||||||
| base_url | auth/oauth | | The base_url is the URL identifier for the service. For example: "https://accounts.google.com". |
|
| LDAPTLS_CERT | ldapTlsCert | ldap | | The LDAP cert's path |
|
||||||
| client_id | auth/oauth | | The OAuth client id. |
|
| LDAPTLS_KEY | ldapTlsKey | ldap | | The LDAP key's path |
|
||||||
| client_secret | auth/oauth | | The OAuth client secret. |
|
| LOG_LEVEL | | | debug | Specify log level, one of: trace, debug, info, off. |
|
||||||
| auth_url | auth/oauth | | The URL for the authentication endpoint. |
|
| LOG_JSON | | | false | Format log output as JSON. |
|
||||||
| token_url | auth/oauth | | The URL for the token endpoint. |
|
| LOG_COLOR | | | true | Colorize log output. |
|
||||||
| redirect_url | auth/oauth | | The redirect URL. |
|
| CONFIG_FILE | | | config.yml | The config file path. |
|
||||||
| 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. |
|
|
||||||
|
|
||||||
|
### Sample yaml configuration
|
||||||
## Upgrading from V1
|
config.yml:
|
||||||
|
```yaml
|
||||||
> :warning: Before upgrading from V1, make sure that you have a backup of your currently working configuration files and database!
|
core:
|
||||||
|
listeningAddress: :8123
|
||||||
To start the upgrade process, start the wg-portal binary with the **-migrateFrom** parameter.
|
externalUrl: https://wg-test.test.com
|
||||||
The configuration (config.yml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
adminUser: test@test.com
|
||||||
|
adminPass: test
|
||||||
To upgrade from a previous SQLite database, start wg-portal like:
|
editableKeys: true
|
||||||
|
createDefaultPeer: false
|
||||||
```shell
|
ldapEnabled: true
|
||||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
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.
|
### RESTful API
|
||||||
For example:
|
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
|
The [API's unittesting](tests/test_API.py) may serve as an example how to make use of the API with python3 & pyswagger.
|
||||||
./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
|
|
||||||
make frontend
|
|
||||||
|
|
||||||
# build the binary
|
|
||||||
make build
|
|
||||||
```
|
|
||||||
|
|
||||||
## What is out of scope
|
## What is out of scope
|
||||||
* Automatic generation or application of any `iptables` or `nftables` rules.
|
* Creating or removing WireGuard (wgX) interfaces.
|
||||||
* Support for operating systems other than linux.
|
* Generation or application of any `iptables` or `nftables` rules.
|
||||||
* Automatic import of private keys of an existing WireGuard setup.
|
* 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
|
## Application stack
|
||||||
|
|
||||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
* [Gin, HTTP web framework written in Go](https://github.com/gin-gonic/gin)
|
||||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
* [go-template, data-driven templates for generating textual output](https://golang.org/pkg/text/template/)
|
||||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
* [Bootstrap, for the HTML templates](https://getbootstrap.com/)
|
||||||
* [Vue.JS](https://vuejs.org/), for the frontend
|
* [JQuery, for some nice JavaScript effects ;)](https://jquery.com/)
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
|
* MIT License. [MIT](LICENSE.txt) or https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
|
||||||
|
This project was inspired by [wg-gen-web](https://github.com/vx3r/wg-gen-web).
|
||||||
|
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;">
|
<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">
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
<tr>
|
<tr>
|
||||||
{{if $.User.Firstname}}
|
{{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>
|
<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}}
|
{{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}}
|
{{end}}
|
||||||
</tr>
|
</tr>
|
||||||
<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: 3,
|
|
||||||
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
"io"
|
||||||
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"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal/common/healthcheck"
|
||||||
"github.com/h44z/wg-portal/internal/adapters"
|
"github.com/h44z/wg-portal/internal/server"
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
evbus "github.com/vardius/message-bus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// main entry point for WireGuard Portal
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
_ = setupLogger(logrus.StandardLogger())
|
||||||
|
|
||||||
logrus.Infof("Starting WireGuard Portal V2...")
|
c := make(chan os.Signal, 1)
|
||||||
logrus.Infof("WireGuard Portal version: %s", internal.Version)
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
cfg, err := config.GetConfig()
|
logrus.Infof("sysinfo: os=%s, arch=%s", runtime.GOOS, runtime.GOARCH)
|
||||||
internal.AssertNoError(err)
|
logrus.Infof("starting WireGuard Portal Server [%s]...", server.Version)
|
||||||
setupLogging(cfg)
|
|
||||||
|
|
||||||
cfg.LogStartupValues()
|
// Context for clean shutdown
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
// start health check service on port 11223
|
||||||
internal.AssertNoError(err)
|
healthcheck.New(healthcheck.ListenOn("127.0.0.1:11223")).StartWithContext(ctx)
|
||||||
|
|
||||||
database, err := adapters.NewSqlRepository(rawDb)
|
service := server.Server{}
|
||||||
internal.AssertNoError(err)
|
if err := service.Setup(ctx); err != nil {
|
||||||
|
logrus.Fatalf("setup failed: %v", 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
queueSize := 100
|
// Attach signal handlers to context
|
||||||
eventBus := evbus.New(queueSize)
|
go func() {
|
||||||
|
osCall := <-c
|
||||||
|
logrus.Tracef("received system call: %v", osCall)
|
||||||
|
cancel() // cancel the context
|
||||||
|
}()
|
||||||
|
|
||||||
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
// Start main process in background
|
||||||
internal.AssertNoError(err)
|
go service.Run()
|
||||||
|
|
||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, eventBus, userManager)
|
<-ctx.Done() // Wait until the context gets canceled
|
||||||
internal.AssertNoError(err)
|
|
||||||
|
|
||||||
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
// Give goroutines some time to stop gracefully
|
||||||
internal.AssertNoError(err)
|
logrus.Info("stopping WireGuard Portal Server...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, database, wireGuard)
|
logrus.Infof("stopped WireGuard Portal Server...")
|
||||||
internal.AssertNoError(err)
|
logrus.Exit(0)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupLogging(cfg *config.Config) {
|
func setupLogger(logger *logrus.Logger) error {
|
||||||
switch strings.ToLower(cfg.Advanced.LogLevel) {
|
// Check environment variables for logrus settings
|
||||||
case "trace":
|
level, ok := os.LookupEnv("LOG_LEVEL")
|
||||||
logrus.SetLevel(logrus.TraceLevel)
|
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":
|
case "debug":
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
logger.SetLevel(logrus.DebugLevel)
|
||||||
case "info", "information":
|
case "trace":
|
||||||
logrus.SetLevel(logrus.InfoLevel)
|
logger.SetLevel(logrus.TraceLevel)
|
||||||
case "warn", "warning":
|
|
||||||
logrus.SetLevel(logrus.WarnLevel)
|
|
||||||
case "error":
|
|
||||||
logrus.SetLevel(logrus.ErrorLevel)
|
|
||||||
default:
|
|
||||||
logrus.SetLevel(logrus.WarnLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
var formatter logrus.Formatter
|
||||||
case cfg.Advanced.LogJson:
|
if useJSON == "false" {
|
||||||
logrus.SetFormatter(&logrus.JSONFormatter{
|
f := new(logrus.TextFormatter)
|
||||||
PrettyPrint: cfg.Advanced.LogPretty,
|
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||||
})
|
f.FullTimestamp = true
|
||||||
case cfg.Advanced.LogPretty:
|
if useColor == "true" {
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
f.ForceColors = true
|
||||||
ForceColors: true,
|
}
|
||||||
DisableColors: false,
|
formatter = f
|
||||||
})
|
} else {
|
||||||
|
f := new(logrus.JSONFormatter)
|
||||||
|
f.TimestampFormat = "2006-01-02 15:04:05"
|
||||||
|
formatter = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.SetFormatter(formatter)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
advanced:
|
|
||||||
log_level: trace
|
|
||||||
|
|
||||||
core:
|
|
||||||
admin_user: test@test.de
|
|
||||||
admin_password: secret
|
|
||||||
create_default_peer: true
|
|
||||||
create_default_peer_on_creation: false
|
|
||||||
|
|
||||||
web:
|
|
||||||
external_url: http://localhost:8888
|
|
||||||
request_logging: true
|
|
||||||
|
|
||||||
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'
|
version: '3.6'
|
||||||
services:
|
services:
|
||||||
wg-portal:
|
wg-portal:
|
||||||
image: wgportal/wg-portal:v2
|
image: wgportal/wg-portal:v1
|
||||||
container_name: wg-portal
|
container_name: wg-portal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
@@ -15,7 +15,5 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /etc/wireguard:/etc/wireguard
|
- /etc/wireguard:/etc/wireguard
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./config:/app/config
|
environment:
|
||||||
# restart: no
|
- EXTERNAL_URL=http://localhost:8123
|
||||||
# command: ["-migrateFrom=/app/data/wg_portal.db"]
|
|
||||||
|
|
||||||
|
@@ -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>
|
|