mirror of
https://github.com/h44z/wg-portal.git
synced 2025-06-28 09:17:00 +00:00
Compare commits
98 Commits
v2.0.0-bet
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
dd28a8dddf | ||
|
f994700caf | ||
|
be29abd29a | ||
|
94785c10ec | ||
|
3a732fd3e5 | ||
|
f0be66aea4 | ||
|
cbf8c5bca9 | ||
|
b6bfa1f6de | ||
|
0c8d6223ce | ||
|
e3b65ca337 | ||
|
61d8aa6589 | ||
|
7fd2bbad02 | ||
|
75a5f3d815 | ||
|
e9005b1b90 | ||
|
8816165260 | ||
|
ab9995350f | ||
|
7df4e4b813 | ||
|
657c4307b3 | ||
|
b918fb6522 | ||
|
78deede360 | ||
|
a8fb4365cf | ||
|
1394be2341 | ||
|
0102588d23 | ||
|
6a96925be7 | ||
|
f018babca7 | ||
|
c6253e7c15 | ||
|
2a1d82251e | ||
|
99d6ce73ad | ||
|
3eb84f0ee9 | ||
|
d8a57edef9 | ||
|
8271dd7c1f | ||
|
4ca37089bc | ||
|
8e5d5138c0 | ||
|
c73286e11a | ||
|
b4aa6f8ef3 | ||
|
432c627f9b | ||
|
cd60761ea7 | ||
|
2c8304417b | ||
|
020ebb64e7 | ||
|
923d4a6188 | ||
|
2b46dca770 | ||
|
b9c4ca04f5 | ||
|
dddf0c475b | ||
|
fe60a5ab9b | ||
|
e176e07f7d | ||
|
b06c03ef8e | ||
|
6b0b78d749 | ||
|
62f3c8d4a1 | ||
|
fbcb22198c | ||
|
2c443a4a9b | ||
|
059234d416 | ||
|
e2966d32ea | ||
|
9354a1d9d3 | ||
|
e75a32e4d0 | ||
|
1d94f6baaf | ||
|
6681dfa96f | ||
|
a60feb7fc9 | ||
|
37904f96fb | ||
|
1e9ee25e49 | ||
|
30eac7c44a | ||
|
801ce76616 | ||
|
5f9c3bab3e | ||
|
e19f42b1eb | ||
|
34fb373659 | ||
|
b938bc8c4c | ||
|
87bf5da5bd | ||
|
3723e4cc75 | ||
|
6cbccf6d43 | ||
|
a49cfa6343 | ||
|
fe681c015c | ||
|
7d0da4e7ad | ||
|
3218bdd6fb | ||
|
12ccd6e32d | ||
|
02ed7b19df | ||
|
678b6c6456 | ||
|
0206952182 | ||
|
53bae9d194 | ||
|
f616a9f5f4 | ||
|
bf5453c264 | ||
|
fd631d3b9f | ||
|
9680e8350c | ||
|
7473132932 | ||
|
5c51573874 | ||
|
fdb436b135 | ||
|
e24acfa57d | ||
|
10332c7f9a | ||
|
f7d7038829 | ||
|
66ccdc29e9 | ||
|
40b4538e78 | ||
|
986f6fdead | ||
|
dabdf111f9 | ||
|
b074af6dc5 | ||
|
eeb0c87c68 | ||
|
67f076effe | ||
|
f6d7a851d1 | ||
|
fc712ebf42 | ||
|
43163273fa | ||
|
5697c2b7f2 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -5,7 +5,7 @@ labels: bug
|
|||||||
|
|
||||||
---
|
---
|
||||||
<!-- Tip: you can use code blocks
|
<!-- Tip: you can use code blocks
|
||||||
for better better formatting of yaml config or logs
|
for better formatting of yaml config or logs
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# config.yaml
|
# config.yaml
|
||||||
|
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@ -28,3 +28,8 @@ updates:
|
|||||||
patch:
|
patch:
|
||||||
update-types:
|
update-types:
|
||||||
- patch
|
- patch
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
14
.github/workflows/docker-publish.yml
vendored
14
.github/workflows/docker-publish.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
push:
|
push:
|
||||||
branches: [master, stable]
|
branches: [master]
|
||||||
# Publish vX.X.X tags as releases.
|
# Publish vX.X.X tags as releases.
|
||||||
tags: ["v*.*.*"]
|
tags: ["v*.*.*"]
|
||||||
|
|
||||||
@ -64,12 +64,12 @@ jobs:
|
|||||||
# major and major.minor tags are not available for alpha or beta releases
|
# major and major.minor tags are not available for alpha or beta releases
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
# add v{{major}} tag, even for beta releases
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
type=semver,pattern=v{{major}}
|
||||||
# add {{major}} tag, even for beta releases
|
# add v{{major}} tag, even for beta or release-canidate releases
|
||||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') }}
|
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||||
# set latest tag for default branch
|
# add {{major}} tag, even for beta releases or release-canidate releases
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
13
.github/workflows/pages.yml
vendored
13
.github/workflows/pages.yml
vendored
@ -2,7 +2,11 @@ name: github-pages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
tags: ["v*"]
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
- '!v*-alpha*'
|
||||||
|
- '!v*-beta*'
|
||||||
|
- '!v*-rc*'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@ -23,6 +27,13 @@ jobs:
|
|||||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||||
|
|
||||||
- name: Publish documentation
|
- name: Publish documentation
|
||||||
|
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
run: mike deploy --push ${{ github.ref_name }}
|
||||||
|
env:
|
||||||
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Publish latest documentation
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||||
env:
|
env:
|
||||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -33,6 +33,7 @@ ssh.key
|
|||||||
wg_portal.db
|
wg_portal.db
|
||||||
sqlite.db
|
sqlite.db
|
||||||
/config.yml
|
/config.yml
|
||||||
|
/config.yaml
|
||||||
/config/
|
/config/
|
||||||
venv/
|
venv/
|
||||||
.cache/
|
.cache/
|
||||||
|
@ -20,7 +20,7 @@ RUN npm run build
|
|||||||
######
|
######
|
||||||
# Build backend
|
# Build backend
|
||||||
######
|
######
|
||||||
FROM --platform=${BUILDPLATFORM} golang:1.23-alpine AS builder
|
FROM --platform=${BUILDPLATFORM} golang:1.24-alpine AS builder
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
@ -50,9 +50,9 @@ COPY --from=builder /build/dist/wg-portal /
|
|||||||
######
|
######
|
||||||
# Final image
|
# Final image
|
||||||
######
|
######
|
||||||
FROM alpine:3.19
|
FROM alpine:3.22
|
||||||
# Install OS-level dependencies
|
# Install OS-level dependencies
|
||||||
RUN apk add --no-cache bash curl iptables nftables openresolv
|
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools
|
||||||
# Setup timezone
|
# Setup timezone
|
||||||
ENV TZ=UTC
|
ENV TZ=UTC
|
||||||
# Copy binaries
|
# Copy binaries
|
||||||
|
31
README.md
31
README.md
@ -1,4 +1,4 @@
|
|||||||
# WireGuard Portal (v2 - testing)
|
# WireGuard Portal v2
|
||||||
|
|
||||||
[](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
|
[](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
@ -8,14 +8,6 @@
|
|||||||

|

|
||||||
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
[](https://hub.docker.com/r/wgportal/wg-portal/)
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> Version 2 is currently under development and may contain bugs and breaking changes.
|
|
||||||
> It is not advised to use this version in production. Use version [v1](https://github.com/h44z/wg-portal/tree/stable) instead.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
|
||||||
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
<!-- Text from this line # is included in docs/documentation/overview.md -->
|
<!-- Text from this line # is included in docs/documentation/overview.md -->
|
||||||
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
|
**WireGuard Portal** is a simple, web-based configuration portal for [WireGuard](https://wireguard.com) server management.
|
||||||
@ -23,27 +15,28 @@ The portal uses the WireGuard [wgctrl](https://github.com/WireGuard/wgctrl-go) l
|
|||||||
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
|
interfaces. This allows for the seamless activation or deactivation of new users without disturbing existing VPN
|
||||||
connections.
|
connections.
|
||||||
|
|
||||||
The configuration portal supports using a database (SQLite, MySQL, MsSQL or Postgres), OAuth or LDAP
|
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.
|
(Active Directory or OpenLDAP) as a user source for authentication and profile data.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Self-hosted - the whole application is a single binary
|
* Self-hosted - the whole application is a single binary
|
||||||
* Responsive multi-language web UI written in Vue.JS
|
* Responsive multi-language web UI written in Vue.js
|
||||||
* Automatically selects IP from the network pool assigned to the client
|
* Automatically selects IP from the network pool assigned to the client
|
||||||
* QR-Code for convenient mobile client configuration
|
* QR-Code for convenient mobile client configuration
|
||||||
* Sends email to the client with QR-code and client config
|
* Sends email to the 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 wg-quick configuration file (`wgX.conf`) if required
|
||||||
* User authentication (database, OAuth, or LDAP)
|
* User authentication (database, OAuth, or LDAP), Passkey support
|
||||||
* IPv6 ready
|
* IPv6 ready
|
||||||
* Docker ready
|
* Docker ready
|
||||||
* Can be used with existing WireGuard setups
|
* Can be used with existing WireGuard setups
|
||||||
* Support for multiple WireGuard interfaces
|
* Support for multiple WireGuard interfaces
|
||||||
* Peer Expiry Feature
|
* Peer Expiry Feature
|
||||||
* Handles route and DNS settings like wg-quick does
|
* Handles route and DNS settings like wg-quick does
|
||||||
* Exposes Prometheus metrics for monitoring and alertingt
|
* Exposes Prometheus metrics for monitoring and alerting
|
||||||
* REST API for management and client deployment
|
* REST API for management and client deployment
|
||||||
|
* Webhook for custom actions on peer, interface, or user updates
|
||||||
|
|
||||||
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
<!-- Text to this line # is included in docs/documentation/overview.md -->
|
||||||

|

|
||||||
@ -52,10 +45,6 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL or Post
|
|||||||
|
|
||||||
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
For the complete documentation visit [wgportal.org](https://wgportal.org).
|
||||||
|
|
||||||
## V2 TODOs
|
|
||||||
|
|
||||||
* Audit UI
|
|
||||||
|
|
||||||
## What is out of scope
|
## What is out of scope
|
||||||
|
|
||||||
* Automatic generation or application of any `iptables` or `nftables` rules.
|
* Automatic generation or application of any `iptables` or `nftables` rules.
|
||||||
@ -65,10 +54,14 @@ For the complete documentation visit [wgportal.org](https://wgportal.org).
|
|||||||
## Application stack
|
## Application stack
|
||||||
|
|
||||||
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
* [wgctrl-go](https://github.com/WireGuard/wgctrl-go) and [netlink](https://github.com/vishvananda/netlink) for interface handling
|
||||||
* [Gin](https://github.com/gin-gonic/gin), HTTP web framework written in Go
|
|
||||||
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
* [Bootstrap](https://getbootstrap.com/), for the HTML templates
|
||||||
* [Vue.JS](https://vuejs.org/), for the frontend
|
* [Vue.js](https://vuejs.org/), for the frontend
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
* MIT License. [MIT](LICENSE.txt) or <https://opensource.org/licenses/MIT>
|
||||||
|
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Since the project was accepted by the Docker-Sponsored Open Source Program, the Docker image location has moved to [wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal).
|
||||||
|
> Please update the Docker image from **h44z/wg-portal** to **wgportal/wg-portal**.
|
||||||
|
@ -5,7 +5,7 @@ If you believe you've found a security issue in one of the supported versions of
|
|||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | -------------------- |
|
|---------|--------------------|
|
||||||
| v2.x | :white_check_mark: |
|
| v2.x | :white_check_mark: |
|
||||||
| v1.x | :white_check_mark: |
|
| v1.x | :white_check_mark: |
|
||||||
|
|
||||||
@ -13,8 +13,8 @@ If you believe you've found a security issue in one of the supported versions of
|
|||||||
|
|
||||||
Please do not report security vulnerabilities through public GitHub issues.
|
Please do not report security vulnerabilities through public GitHub issues.
|
||||||
|
|
||||||
Instead, we encourage you to submit a report through Github [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
|
Instead, we encourage you to submit a report through GitHub [private vulnerability reporting](https://github.com/h44z/wg-portal/security).
|
||||||
If you prefer to submit a report without logging in to Github, please email *info (at) wgportal.org*.
|
If you prefer to submit a report without logging in to GitHub, please email *info (at) wgportal.org*.
|
||||||
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
|
We will respond as soon as possible, but as only two people currently maintain this project, we cannot guarantee specific response times.
|
||||||
|
|
||||||
We prefer all communications to be in English.
|
We prefer all communications to be in English.
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/swaggo/swag"
|
"github.com/swaggo/swag"
|
||||||
"github.com/swaggo/swag/gen"
|
"github.com/swaggo/swag/gen"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var apiRootPath = "/internal/app/api"
|
var apiRootPath = "/internal/app/api"
|
||||||
@ -100,7 +100,7 @@ func copyDocForMkdocs(workingDir, basePath, version string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
func removeAuthorizeButton(input []byte) ([]byte, error) {
|
||||||
var swagger map[string]interface{}
|
var swagger map[string]any
|
||||||
err := yaml.Unmarshal(input, &swagger)
|
err := yaml.Unmarshal(input, &swagger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
return nil, fmt.Errorf("error while unmarshalling swagger file: %w", err)
|
||||||
|
@ -2,12 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
evbus "github.com/vardius/message-bus"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
|
"github.com/h44z/wg-portal/internal/adapters"
|
||||||
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||||
|
backendV0 "github.com/h44z/wg-portal/internal/app/api/v0/backend"
|
||||||
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
handlersV0 "github.com/h44z/wg-portal/internal/app/api/v0/handlers"
|
||||||
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
backendV1 "github.com/h44z/wg-portal/internal/app/api/v1/backend"
|
||||||
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
handlersV1 "github.com/h44z/wg-portal/internal/app/api/v1/handlers"
|
||||||
@ -17,29 +25,25 @@ import (
|
|||||||
"github.com/h44z/wg-portal/internal/app/mail"
|
"github.com/h44z/wg-portal/internal/app/mail"
|
||||||
"github.com/h44z/wg-portal/internal/app/route"
|
"github.com/h44z/wg-portal/internal/app/route"
|
||||||
"github.com/h44z/wg-portal/internal/app/users"
|
"github.com/h44z/wg-portal/internal/app/users"
|
||||||
|
"github.com/h44z/wg-portal/internal/app/webhooks"
|
||||||
"github.com/h44z/wg-portal/internal/app/wireguard"
|
"github.com/h44z/wg-portal/internal/app/wireguard"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
|
||||||
"github.com/h44z/wg-portal/internal/adapters"
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
evbus "github.com/vardius/message-bus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// main entry point for WireGuard Portal
|
// main entry point for WireGuard Portal
|
||||||
func main() {
|
func main() {
|
||||||
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
ctx := internal.SignalAwareContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
logrus.Infof("Starting WireGuard Portal V2...")
|
slog.Info("Starting WireGuard Portal V2...", "version", internal.Version)
|
||||||
logrus.Infof("WireGuard Portal version: %s", internal.Version)
|
|
||||||
|
|
||||||
cfg, err := config.GetConfig()
|
cfg, err := config.GetConfig()
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
setupLogging(cfg)
|
internal.SetupLogging(cfg.Advanced.LogLevel, cfg.Advanced.LogPretty, cfg.Advanced.LogJson)
|
||||||
|
|
||||||
cfg.LogStartupValues()
|
cfg.LogStartupValues()
|
||||||
|
|
||||||
|
dbEncryptedSerializer := app.NewGormEncryptedStringSerializer(cfg.Database.EncryptionPassphrase)
|
||||||
|
schema.RegisterSerializer("encstr", dbEncryptedSerializer)
|
||||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
@ -57,31 +61,43 @@ func main() {
|
|||||||
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
|
cfgFileSystem, err := adapters.NewFileSystemRepository(cfg.Advanced.ConfigStoragePath)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
shouldExit, err := app.HandleProgramArgs(cfg, rawDb)
|
shouldExit, err := app.HandleProgramArgs(rawDb)
|
||||||
switch {
|
switch {
|
||||||
case shouldExit && err == nil:
|
case shouldExit && err == nil:
|
||||||
return
|
return
|
||||||
case shouldExit && err != nil:
|
case shouldExit:
|
||||||
logrus.Errorf("Failed to process program args: %v", err)
|
slog.Error("Failed to process program args", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
case !shouldExit:
|
default:
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
queueSize := 100
|
queueSize := 100
|
||||||
eventBus := evbus.New(queueSize)
|
eventBus := evbus.New(queueSize)
|
||||||
|
|
||||||
|
auditManager := audit.NewManager(database)
|
||||||
|
|
||||||
|
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
||||||
|
internal.AssertNoError(err)
|
||||||
|
auditRecorder.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
userManager, err := users.NewUserManager(cfg, eventBus, database, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
userManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
|
webAuthn, err := auth.NewWebAuthnAuthenticator(cfg, eventBus, userManager)
|
||||||
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
wireGuardManager, err := wireguard.NewWireGuardManager(cfg, eventBus, wireGuard, wgQuick, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
wireGuardManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
|
statisticsCollector, err := wireguard.NewStatisticsCollector(cfg, eventBus, database, wireGuard, metricsServer)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
statisticsCollector.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
|
cfgFileManager, err := configfile.NewConfigFileManager(cfg, eventBus, database, database, cfgFileSystem)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
@ -89,35 +105,66 @@ func main() {
|
|||||||
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
mailManager, err := mail.NewMailManager(cfg, mailer, cfgFileManager, database, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
auditRecorder, err := audit.NewAuditRecorder(cfg, eventBus, database)
|
|
||||||
internal.AssertNoError(err)
|
|
||||||
auditRecorder.StartBackgroundJobs(ctx)
|
|
||||||
|
|
||||||
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
routeManager, err := route.NewRouteManager(cfg, eventBus, database)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
routeManager.StartBackgroundJobs(ctx)
|
routeManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager,
|
webhookManager, err := webhooks.NewManager(cfg, eventBus)
|
||||||
statisticsCollector, cfgFileManager, mailManager)
|
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
err = backend.Startup(ctx)
|
webhookManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
|
err = app.Initialize(cfg, wireGuardManager, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(cfg, backend)
|
validatorManager := validator.New()
|
||||||
|
|
||||||
|
// region API v0 (SPA frontend)
|
||||||
|
|
||||||
|
apiV0Session := handlersV0.NewSessionWrapper(cfg)
|
||||||
|
apiV0Auth := handlersV0.NewAuthenticationHandler(authenticator, apiV0Session)
|
||||||
|
|
||||||
|
apiV0BackendUsers := backendV0.NewUserService(cfg, userManager, wireGuardManager)
|
||||||
|
apiV0BackendInterfaces := backendV0.NewInterfaceService(cfg, wireGuardManager, cfgFileManager)
|
||||||
|
apiV0BackendPeers := backendV0.NewPeerService(cfg, wireGuardManager, cfgFileManager, mailManager)
|
||||||
|
|
||||||
|
apiV0EndpointAuth := handlersV0.NewAuthEndpoint(cfg, apiV0Auth, apiV0Session, validatorManager, authenticator,
|
||||||
|
webAuthn)
|
||||||
|
apiV0EndpointAudit := handlersV0.NewAuditEndpoint(cfg, apiV0Auth, auditManager)
|
||||||
|
apiV0EndpointUsers := handlersV0.NewUserEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendUsers)
|
||||||
|
apiV0EndpointInterfaces := handlersV0.NewInterfaceEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendInterfaces)
|
||||||
|
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||||
|
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth)
|
||||||
|
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||||
|
|
||||||
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
|
apiV0EndpointAuth,
|
||||||
|
apiV0EndpointAudit,
|
||||||
|
apiV0EndpointUsers,
|
||||||
|
apiV0EndpointInterfaces,
|
||||||
|
apiV0EndpointPeers,
|
||||||
|
apiV0EndpointConfig,
|
||||||
|
apiV0EndpointTest,
|
||||||
|
)
|
||||||
|
|
||||||
|
// endregion API v0 (SPA frontend)
|
||||||
|
|
||||||
|
// region API v1 (User REST API)
|
||||||
|
|
||||||
|
apiV1Auth := handlersV1.NewAuthenticationHandler(userManager)
|
||||||
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
|
||||||
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
|
||||||
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
|
||||||
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
|
||||||
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager)
|
||||||
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers)
|
|
||||||
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers)
|
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1Auth, validatorManager, apiV1BackendUsers)
|
||||||
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces)
|
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1Auth, validatorManager, apiV1BackendPeers)
|
||||||
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning)
|
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1Auth, validatorManager, apiV1BackendInterfaces)
|
||||||
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1BackendMetrics)
|
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1Auth, validatorManager,
|
||||||
|
apiV1BackendProvisioning)
|
||||||
|
apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1Auth, validatorManager, apiV1BackendMetrics)
|
||||||
|
|
||||||
apiV1 := handlersV1.NewRestApi(
|
apiV1 := handlersV1.NewRestApi(
|
||||||
userManager,
|
|
||||||
apiV1EndpointUsers,
|
apiV1EndpointUsers,
|
||||||
apiV1EndpointPeers,
|
apiV1EndpointPeers,
|
||||||
apiV1EndpointInterfaces,
|
apiV1EndpointInterfaces,
|
||||||
@ -125,47 +172,22 @@ func main() {
|
|||||||
apiV1EndpointMetrics,
|
apiV1EndpointMetrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// endregion API v1 (User REST API)
|
||||||
|
|
||||||
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
|
|
||||||
go metricsServer.Run(ctx)
|
go metricsServer.Run(ctx)
|
||||||
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
go webSrv.Run(ctx, cfg.Web.ListeningAddress)
|
||||||
|
|
||||||
|
slog.Info("Application startup complete")
|
||||||
|
|
||||||
// wait until context gets cancelled
|
// wait until context gets cancelled
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
logrus.Infof("Stopping WireGuard Portal")
|
slog.Info("Stopping WireGuard Portal")
|
||||||
|
|
||||||
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
time.Sleep(5 * time.Second) // wait for (most) goroutines to finish gracefully
|
||||||
|
|
||||||
logrus.Infof("Stopped WireGuard Portal")
|
slog.Info("Stopped WireGuard Portal")
|
||||||
}
|
|
||||||
|
|
||||||
func setupLogging(cfg *config.Config) {
|
|
||||||
switch strings.ToLower(cfg.Advanced.LogLevel) {
|
|
||||||
case "trace":
|
|
||||||
logrus.SetLevel(logrus.TraceLevel)
|
|
||||||
case "debug":
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
|
||||||
case "info", "information":
|
|
||||||
logrus.SetLevel(logrus.InfoLevel)
|
|
||||||
case "warn", "warning":
|
|
||||||
logrus.SetLevel(logrus.WarnLevel)
|
|
||||||
case "error":
|
|
||||||
logrus.SetLevel(logrus.ErrorLevel)
|
|
||||||
default:
|
|
||||||
logrus.SetLevel(logrus.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case cfg.Advanced.LogJson:
|
|
||||||
logrus.SetFormatter(&logrus.JSONFormatter{
|
|
||||||
PrettyPrint: cfg.Advanced.LogPretty,
|
|
||||||
})
|
|
||||||
case cfg.Advanced.LogPretty:
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{
|
|
||||||
ForceColors: true,
|
|
||||||
DisableColors: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,15 @@ web:
|
|||||||
external_url: http://localhost:8888
|
external_url: http://localhost:8888
|
||||||
request_logging: true
|
request_logging: true
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
url: ""
|
||||||
|
authentication: ""
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
auth:
|
auth:
|
||||||
ldap:
|
ldap:
|
||||||
- id: ldap1
|
- id: ldap1
|
||||||
provider_name: company ldap
|
provider_name: company ldap
|
||||||
display_name: Login with</br>LDAP
|
|
||||||
url: ldap://ldap.yourcompany.local:389
|
url: ldap://ldap.yourcompany.local:389
|
||||||
bind_user: ldap_wireguard@yourcompany.local
|
bind_user: ldap_wireguard@yourcompany.local
|
||||||
bind_pass: super_Secret_PASSWORD
|
bind_pass: super_Secret_PASSWORD
|
||||||
|
@ -16,7 +16,7 @@ annotations:
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 0.7.0
|
version: 0.7.1
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# wg-portal
|
# wg-portal
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
## Values
|
## Values
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
|----------------------------------|------------|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----|------|---------|-------------|
|
||||||
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
| nameOverride | string | `""` | Partially override resource names (adds suffix) |
|
||||||
| fullnameOverride | string | `""` | Fully override resource names |
|
| fullnameOverride | string | `""` | Fully override resource names |
|
||||||
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
| extraDeploy | list | `[]` | Array of extra objects to deploy with the release |
|
||||||
@ -102,6 +102,7 @@ The [Values](#values) section lists the parameters that can be configured during
|
|||||||
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
| persistence.storageClass | string | `""` | Persistent Volume storage class. If undefined (the default) cluster's default provisioner will be used. |
|
||||||
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
| persistence.accessMode | string | `"ReadWriteOnce"` | Persistent Volume Access Mode |
|
||||||
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
| persistence.size | string | `"1Gi"` | Persistent Volume size |
|
||||||
|
| persistence.volumeName | string | `""` | Persistent Volume Name (optional) |
|
||||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||||
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
| serviceAccount.annotations | object | `{}` | Service account annotations |
|
||||||
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
| serviceAccount.automount | bool | `false` | Automatically mount a ServiceAccount's API credentials |
|
||||||
|
@ -89,13 +89,17 @@ admin_user: ""
|
|||||||
Define PersistentVolumeClaim spec
|
Define PersistentVolumeClaim spec
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "wg-portal.pvc" -}}
|
{{- define "wg-portal.pvc" -}}
|
||||||
accessModes: [{{ .Values.persistence.accessMode }}]
|
accessModes:
|
||||||
{{- with .Values.persistence.storageClass }}
|
- {{ .Values.persistence.accessMode }}
|
||||||
storageClassName: {{ . }}
|
|
||||||
{{- end }}
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: {{ .Values.persistence.size | quote }}
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- with .Values.persistence.storageClass }}
|
||||||
|
storageClassName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.persistence.volumeName }}
|
||||||
|
volumeName: {{ . }}
|
||||||
|
{{- end }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
|
@ -195,6 +195,8 @@ persistence:
|
|||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
# -- Persistent Volume size
|
# -- Persistent Volume size
|
||||||
size: 1Gi
|
size: 1Gi
|
||||||
|
# -- Persistent Volume Name (optional)
|
||||||
|
volumeName: ""
|
||||||
|
|
||||||
serviceAccount:
|
serviceAccount:
|
||||||
# -- Specifies whether a service account should be created
|
# -- Specifies whether a service account should be created
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
wg-portal:
|
wg-portal:
|
||||||
image: wgportal/wg-portal:latest
|
image: wgportal/wg-portal:v2
|
||||||
container_name: wg-portal
|
container_name: wg-portal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
logging:
|
logging:
|
||||||
@ -10,8 +10,10 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
# Use host network mode for WireGuard and the UI. Ensure that access to the UI is properly secured.
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
volumes:
|
volumes:
|
||||||
|
# left side is the host path, right side is the container path
|
||||||
- /etc/wireguard:/etc/wireguard
|
- /etc/wireguard:/etc/wireguard
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
|
BIN
docs/assets/images/interface_view.png
Normal file
BIN
docs/assets/images/interface_view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
BIN
docs/assets/images/landing_page.png
Normal file
BIN
docs/assets/images/landing_page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
docs/assets/images/passkey_setup.png
Normal file
BIN
docs/assets/images/passkey_setup.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
@ -15,7 +15,7 @@ web:
|
|||||||
site_title: My WireGuard Server
|
site_title: My WireGuard Server
|
||||||
site_company_name: My Company
|
site_company_name: My Company
|
||||||
listening_address: :8080
|
listening_address: :8080
|
||||||
external_url: https://my.externa-domain.com
|
external_url: https://my.external-domain.com
|
||||||
csrf_secret: super-s3cr3t-csrf
|
csrf_secret: super-s3cr3t-csrf
|
||||||
session_secret: super-s3cr3t-session
|
session_secret: super-s3cr3t-session
|
||||||
request_logging: true
|
request_logging: true
|
||||||
@ -31,6 +31,11 @@ database:
|
|||||||
debug: true
|
debug: true
|
||||||
type: sqlite
|
type: sqlite
|
||||||
dsn: data/sqlite.db
|
dsn: data/sqlite.db
|
||||||
|
encryption_passphrase: change-this-s3cr3t-encryption-passphrase
|
||||||
|
|
||||||
|
auth:
|
||||||
|
webauthn:
|
||||||
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
## LDAP Authentication and Synchronization
|
## LDAP Authentication and Synchronization
|
||||||
@ -43,7 +48,6 @@ auth:
|
|||||||
# a sample LDAP provider with user sync enabled
|
# a sample LDAP provider with user sync enabled
|
||||||
- id: ldap
|
- id: ldap
|
||||||
provider_name: Active Directory
|
provider_name: Active Directory
|
||||||
display_name: Login with</br>AD
|
|
||||||
url: ldap://srv-ad1.company.local:389
|
url: ldap://srv-ad1.company.local:389
|
||||||
bind_user: ldap_wireguard@company.local
|
bind_user: ldap_wireguard@company.local
|
||||||
bind_pass: super-s3cr3t-ldap
|
bind_pass: super-s3cr3t-ldap
|
||||||
@ -72,7 +76,8 @@ auth:
|
|||||||
|
|
||||||
auth:
|
auth:
|
||||||
oidc:
|
oidc:
|
||||||
# a sample Entra ID provider with environment variable substitution
|
# A sample Entra ID provider with environment variable substitution.
|
||||||
|
# Only users with an @outlook.com email address are allowed to register or login.
|
||||||
- id: azure
|
- id: azure
|
||||||
provider_name: azure
|
provider_name: azure
|
||||||
display_name: Login with</br>Entra ID
|
display_name: Login with</br>Entra ID
|
||||||
@ -80,6 +85,8 @@ auth:
|
|||||||
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
||||||
client_id: "${AZURE_CLIENT_ID}"
|
client_id: "${AZURE_CLIENT_ID}"
|
||||||
client_secret: "${AZURE_CLIENT_SECRET}"
|
client_secret: "${AZURE_CLIENT_SECRET}"
|
||||||
|
allowed_domains:
|
||||||
|
- "outlook.com"
|
||||||
extra_scopes:
|
extra_scopes:
|
||||||
- profile
|
- profile
|
||||||
- email
|
- email
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
This page provides an overview of **all available configuration options** for WireGuard Portal.
|
||||||
|
|
||||||
You can supply these configurations in a **YAML** file (e.g. `config.yaml`) when starting the Portal.
|
You can supply these configurations in a **YAML** file when starting the Portal.
|
||||||
The path of the configuration file defaults to **config/config.yml** in the working directory of the executable.
|
The path of the configuration file defaults to `config/config.yaml` (or `config/config.yml`) in the working directory of the executable.
|
||||||
It is possible to override configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
It is possible to override the configuration filepath using the environment variable `WG_PORTAL_CONFIG`.
|
||||||
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
|
For example: `WG_PORTAL_CONFIG=/etc/wg-portal/config.yaml ./wg-portal`.
|
||||||
Also, environment variable substitution in config file is supported. Refer to [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
|
Also, environment variable substitution in the config file is supported. Refer to the [syntax](https://github.com/a8m/envsubst?tab=readme-ov-file#docs).
|
||||||
|
|
||||||
Configuration examples are available on the [Examples](./examples.md) page.
|
Configuration examples are available on the [Examples](./examples.md) page.
|
||||||
|
|
||||||
@ -14,7 +14,8 @@ Configuration examples are available on the [Examples](./examples.md) page.
|
|||||||
```yaml
|
```yaml
|
||||||
core:
|
core:
|
||||||
admin_user: admin@wgportal.local
|
admin_user: admin@wgportal.local
|
||||||
admin_password: wgportal
|
admin_password: wgportal-default
|
||||||
|
admin_api_token: ""
|
||||||
editable_keys: true
|
editable_keys: true
|
||||||
create_default_peer: false
|
create_default_peer: false
|
||||||
create_default_peer_on_creation: false
|
create_default_peer_on_creation: false
|
||||||
@ -35,13 +36,16 @@ advanced:
|
|||||||
config_storage_path: ""
|
config_storage_path: ""
|
||||||
expiry_check_interval: 15m
|
expiry_check_interval: 15m
|
||||||
rule_prio_offset: 20000
|
rule_prio_offset: 20000
|
||||||
|
route_table_offset: 20000
|
||||||
api_admin_only: true
|
api_admin_only: true
|
||||||
|
limit_additional_user_peers: 0
|
||||||
|
|
||||||
database:
|
database:
|
||||||
debug: false
|
debug: false
|
||||||
slow_query_threshold: 0
|
slow_query_threshold: "0"
|
||||||
type: sqlite
|
type: sqlite
|
||||||
dsn: data/sqlite.db
|
dsn: data/sqlite.db
|
||||||
|
encryption_passphrase: ""
|
||||||
|
|
||||||
statistics:
|
statistics:
|
||||||
use_ping_checks: true
|
use_ping_checks: true
|
||||||
@ -58,7 +62,7 @@ mail:
|
|||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 25
|
port: 25
|
||||||
encryption: none
|
encryption: none
|
||||||
cert_validation: false
|
cert_validation: true
|
||||||
username: ""
|
username: ""
|
||||||
password: ""
|
password: ""
|
||||||
auth_type: plain
|
auth_type: plain
|
||||||
@ -69,6 +73,10 @@ auth:
|
|||||||
oidc: []
|
oidc: []
|
||||||
oauth: []
|
oauth: []
|
||||||
ldap: []
|
ldap: []
|
||||||
|
webauthn:
|
||||||
|
enabled: true
|
||||||
|
min_password_length: 16
|
||||||
|
hide_login_form: false
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listening_address: :8888
|
listening_address: :8888
|
||||||
@ -79,8 +87,14 @@ web:
|
|||||||
session_secret: very_secret
|
session_secret: very_secret
|
||||||
csrf_secret: extremely_secret
|
csrf_secret: extremely_secret
|
||||||
request_logging: false
|
request_logging: false
|
||||||
|
expose_host_info: false
|
||||||
cert_file: ""
|
cert_file: ""
|
||||||
key_File: ""
|
key_File: ""
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
url: ""
|
||||||
|
authentication: ""
|
||||||
|
timeout: 10s
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@ -92,8 +106,9 @@ Below you will find sections like
|
|||||||
[`database`](#database),
|
[`database`](#database),
|
||||||
[`statistics`](#statistics),
|
[`statistics`](#statistics),
|
||||||
[`mail`](#mail),
|
[`mail`](#mail),
|
||||||
[`auth`](#auth) and
|
[`auth`](#auth),
|
||||||
[`web`](#web).
|
[`web`](#web) and
|
||||||
|
[`webhook`](#webhook).
|
||||||
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
Each section describes the individual configuration keys, their default values, and a brief explanation of their purpose.
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -108,8 +123,9 @@ More advanced options are found in the subsequent `Advanced` section.
|
|||||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||||
|
|
||||||
### `admin_password`
|
### `admin_password`
|
||||||
- **Default:** `wgportal`
|
- **Default:** `wgportal-default`
|
||||||
- **Description:** The administrator password. The default password of `wgportal` should be changed immediately.
|
- **Description:** The administrator password. The default password should be changed immediately!
|
||||||
|
- **Important:** The password should be strong and secure. The minimum password length is specified in [auth.min_password_length](#min_password_length). By default, it is 16 characters.
|
||||||
|
|
||||||
### `admin_api_token`
|
### `admin_api_token`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@ -201,6 +217,10 @@ Additional or more specialized configuration options for logging and interface c
|
|||||||
- **Default:** `true`
|
- **Default:** `true`
|
||||||
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
- **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md).
|
||||||
|
|
||||||
|
### `limit_additional_user_peers`
|
||||||
|
- **Default:** `0`
|
||||||
|
- **Description:** Limit additional peers a normal user can create. `0` means unlimited.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
@ -208,13 +228,15 @@ Additional or more specialized configuration options for logging and interface c
|
|||||||
Configuration for the underlying database used by WireGuard Portal.
|
Configuration for the underlying database used by WireGuard Portal.
|
||||||
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
||||||
|
|
||||||
|
If sensitive values (like private keys) should be stored in an encrypted format, set the `encryption_passphrase` option.
|
||||||
|
|
||||||
### `debug`
|
### `debug`
|
||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Description:** If `true`, logs all database statements (verbose).
|
- **Description:** If `true`, logs all database statements (verbose).
|
||||||
|
|
||||||
### `slow_query_threshold`
|
### `slow_query_threshold`
|
||||||
- **Default:** 0
|
- **Default:** "0"
|
||||||
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If empty or zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
- **Description:** A time threshold (e.g., `100ms`) above which queries are considered slow and logged as warnings. If zero, slow query logging is disabled. Format uses `s`, `ms` for seconds, milliseconds, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration). The value must be a string.
|
||||||
|
|
||||||
### `type`
|
### `type`
|
||||||
- **Default:** `sqlite`
|
- **Default:** `sqlite`
|
||||||
@ -228,6 +250,12 @@ Supported databases include SQLite, MySQL, Microsoft SQL Server, and Postgres.
|
|||||||
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `encryption_passphrase`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** Passphrase for encrypting sensitive values such as private keys in the database. Encryption is only applied if this passphrase is set.
|
||||||
|
**Important:** Once you enable encryption by setting this passphrase, you cannot disable it or change it afterward.
|
||||||
|
New or updated records will be encrypted; existing data remains in plaintext until it’s next modified.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Statistics
|
## Statistics
|
||||||
@ -268,7 +296,7 @@ Controls how WireGuard Portal collects and reports usage statistics, including p
|
|||||||
|
|
||||||
### `listening_address`
|
### `listening_address`
|
||||||
- **Default:** `:8787`
|
- **Default:** `:8787`
|
||||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787`).
|
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -289,7 +317,7 @@ Options for configuring email notifications or sending peer configurations via e
|
|||||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||||
|
|
||||||
### `cert_validation`
|
### `cert_validation`
|
||||||
- **Default:** `false`
|
- **Default:** `true`
|
||||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||||
|
|
||||||
### `username`
|
### `username`
|
||||||
@ -316,9 +344,23 @@ Options for configuring email notifications or sending peer configurations via e
|
|||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
|
|
||||||
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), and **LDAP** (`ldap`).
|
WireGuard Portal supports multiple authentication strategies, including **OpenID Connect** (`oidc`), **OAuth** (`oauth`), **Passkeys** (`webauthn`) and **LDAP** (`ldap`).
|
||||||
Each can have multiple providers configured. Below are the relevant keys.
|
Each can have multiple providers configured. Below are the relevant keys.
|
||||||
|
|
||||||
|
Some core authentication options are shared across all providers, while others are specific to each provider type.
|
||||||
|
|
||||||
|
### `min_password_length`
|
||||||
|
- **Default:** `16`
|
||||||
|
- **Description:** Minimum password length for local authentication. This is not enforced for LDAP authentication.
|
||||||
|
The default admin password strength is also enforced by this setting.
|
||||||
|
- **Important:** The password should be strong and secure. It is recommended to use a password with at least 16 characters, including uppercase and lowercase letters, numbers, and special characters.
|
||||||
|
|
||||||
|
### `hide_login_form`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** If `true`, the login form is hidden and only the OIDC, OAuth, LDAP, or WebAuthn providers are shown. This is useful if you want to enforce a specific authentication method.
|
||||||
|
If no social login providers are configured, the login form is always shown, regardless of this setting.
|
||||||
|
- **Important:** You can still access the login form by adding the `?all` query parameter to the login URL (e.g. https://wg.portal/#/login?all).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### OIDC
|
### OIDC
|
||||||
@ -350,13 +392,17 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
|
||||||
|
|
||||||
|
#### `allowed_domains`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
|
||||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||||
|
|
||||||
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
| **Field** | **Typical OIDC Claim** | **Explanation** |
|
||||||
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||||
@ -419,13 +465,17 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** A list of OAuth scopes.
|
- **Description:** A list of OAuth scopes.
|
||||||
|
|
||||||
|
#### `allowed_domains`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
|
||||||
|
|
||||||
#### `field_map`
|
#### `field_map`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
- **Description:** Maps OAuth attributes to WireGuard Portal fields.
|
||||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `is_admin`, `user_groups`.
|
||||||
|
|
||||||
| **Field** | **Typical Claim** | **Explanation** |
|
| **Field** | **Typical Claim** | **Explanation** |
|
||||||
| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|-------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
| `user_identifier` | `sub` or `preferred_username` | A unique identifier for the user. Often the OIDC `sub` claim is used because it’s guaranteed to be unique for the user within the IdP. Some providers also support `preferred_username` if it’s unique. |
|
||||||
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
| `email` | `email` | The user’s email address as provided by the IdP. Not always verified, depending on IdP settings. |
|
||||||
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
| `firstname` | `given_name` | The user’s first name, typically provided by the IdP in the `given_name` claim. |
|
||||||
@ -456,6 +506,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
|||||||
The `ldap` array contains a list of LDAP authentication providers.
|
The `ldap` array contains a list of LDAP authentication providers.
|
||||||
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||||
|
|
||||||
|
#### `provider_name`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** A **unique** name for this provider. Must not conflict with other providers.
|
||||||
|
|
||||||
#### `url`
|
#### `url`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||||
@ -494,7 +548,7 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
- Available fields: `user_identifier`, `email`, `firstname`, `lastname`, `phone`, `department`, `memberof`.
|
||||||
|
|
||||||
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
| **WireGuard Portal Field** | **Typical LDAP Attribute** | **Short Description** |
|
||||||
| -------------------------- | -------------------------- | ------------------------------------------------------------ |
|
|----------------------------|----------------------------|--------------------------------------------------------------|
|
||||||
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
| user_identifier | sAMAccountName / uid | Uniquely identifies the user within the LDAP directory. |
|
||||||
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
| email | mail / userPrincipalName | Stores the user's primary email address. |
|
||||||
| firstname | givenName | Contains the user's first (given) name. |
|
| firstname | givenName | Contains the user's first (given) name. |
|
||||||
@ -510,6 +564,8 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
```text
|
```text
|
||||||
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))
|
||||||
```
|
```
|
||||||
|
- **Important**: The `login_filter` must always be a valid LDAP filter. It should at most return one user.
|
||||||
|
If the filter returns multiple or no users, the login will fail.
|
||||||
|
|
||||||
#### `admin_group`
|
#### `admin_group`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
@ -550,15 +606,31 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### WebAuthn (Passkeys)
|
||||||
|
|
||||||
|
The `webauthn` section contains configuration options for WebAuthn authentication (passkeys).
|
||||||
|
|
||||||
|
#### `enabled`
|
||||||
|
- **Default:** `true`
|
||||||
|
- **Description:** If `true`, Passkey authentication is enabled. If `false`, WebAuthn is disabled.
|
||||||
|
Users are encouraged to use Passkeys for secure authentication instead of passwords.
|
||||||
|
If a passkey is registered, the password login is still available as a fallback. Ensure that the password is strong and secure.
|
||||||
|
|
||||||
## Web
|
## Web
|
||||||
|
|
||||||
|
The web section contains configuration options for the web server, including the listening address, session management, and CSRF protection.
|
||||||
|
It is important to specify a valid `external_url` for the web server, especially if you are using a reverse proxy.
|
||||||
|
Without a valid `external_url`, the login process may fail due to CSRF protection.
|
||||||
|
|
||||||
### `listening_address`
|
### `listening_address`
|
||||||
- **Default:** `:8888`
|
- **Default:** `:8888`
|
||||||
- **Description:** The listening port of the web server.
|
- **Description:** The listening address and port for the web server (e.g., `:8888` to bind on all interfaces or `127.0.0.1:8888` to bind only on the loopback interface).
|
||||||
|
Ensure that access to WireGuard Portal is protected against unauthorized access, especially if binding to all interfaces.
|
||||||
|
|
||||||
### `external_url`
|
### `external_url`
|
||||||
- **Default:** `http://localhost:8888`
|
- **Default:** `http://localhost:8888`
|
||||||
- **Description:** The URL where a client can access WireGuard Portal.
|
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||||
|
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
||||||
|
|
||||||
### `site_company_name`
|
### `site_company_name`
|
||||||
- **Default:** `WireGuard Portal`
|
- **Default:** `WireGuard Portal`
|
||||||
@ -584,6 +656,10 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
- **Description:** Log all HTTP requests.
|
- **Description:** Log all HTTP requests.
|
||||||
|
|
||||||
|
### `expose_host_info`
|
||||||
|
- **Default:** `false`
|
||||||
|
- **Description:** Expose the hostname and version of the WireGuard Portal server in an HTTP header. This is useful for debugging but may expose sensitive information.
|
||||||
|
|
||||||
### `cert_file`
|
### `cert_file`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** (Optional) Path to the TLS certificate file.
|
- **Description:** (Optional) Path to the TLS certificate file.
|
||||||
@ -591,3 +667,35 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
|||||||
### `key_file`
|
### `key_file`
|
||||||
- **Default:** *(empty)*
|
- **Default:** *(empty)*
|
||||||
- **Description:** (Optional) Path to the TLS certificate key file.
|
- **Description:** (Optional) Path to the TLS certificate key file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Webhook
|
||||||
|
|
||||||
|
The webhook section allows you to configure a webhook that is called on certain events in WireGuard Portal.
|
||||||
|
A JSON object is sent in a POST request to the webhook URL with the following structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "update",
|
||||||
|
"entity": "peer",
|
||||||
|
"identifier": "the-peer-identifier",
|
||||||
|
"payload": {
|
||||||
|
// The payload of the event, e.g. peer data.
|
||||||
|
// Check the API documentation for the exact structure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Further details can be found in the [usage documentation](../usage/webhooks.md).
|
||||||
|
|
||||||
|
### `url`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled.
|
||||||
|
|
||||||
|
### `authentication`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Description:** The Authorization header for the webhook endpoint. The value is send as-is in the header. For example: `Bearer <token>`.
|
||||||
|
|
||||||
|
### `timeout`
|
||||||
|
- **Default:** `10s`
|
||||||
|
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.
|
@ -3,23 +3,31 @@ These binary versions can be manually downloaded and installed.
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
|
Make sure that you download the correct binary for your architecture. The available binaries are:
|
||||||
|
|
||||||
|
- `wg-portal_linux_amd64` - Linux x86_64
|
||||||
|
- `wg-portal_linux_arm64` - Linux ARM 64-bit
|
||||||
|
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
|
||||||
|
|
||||||
With `curl`:
|
With `curl`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
curl -L -o wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
With `wget`:
|
With `wget`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
wget -O wg-portal https://github.com/h44z/wg-portal/releases/download/${WG_PORTAL_VERSION}/wg-portal_linux_amd64
|
||||||
```
|
```
|
||||||
|
|
||||||
with `gh cli`:
|
with `gh cli`:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@ -28,7 +36,7 @@ sudo mkdir -p /opt/wg-portal
|
|||||||
sudo install wg-portal /opt/wg-portal/
|
sudo install wg-portal /opt/wg-portal/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased versions (master branch builds)
|
||||||
|
|
||||||
|
Unreleased versions can be fetched directly from the artifacts section of the [GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster).
|
||||||
|
|
||||||
Unreleased versions could be downloaded from
|
|
||||||
[GitHub Workflow](https://github.com/h44z/wg-portal/actions/workflows/docker-publish.yml?query=branch%3Amaster) artifacs also.
|
|
||||||
|
@ -1,57 +1,161 @@
|
|||||||
## Image Usage
|
## Image Usage
|
||||||
|
|
||||||
The preferred way to start WireGuard Portal as Docker container is to use Docker Compose.
|
The WireGuard Portal Docker image is available on both [Docker Hub](https://hub.docker.com/r/wgportal/wg-portal) and [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
||||||
|
It is built on the official Alpine Linux base image and comes pre-packaged with all necessary WireGuard dependencies.
|
||||||
|
|
||||||
A sample docker-compose.yml:
|
This container allows you to establish WireGuard VPN connections without relying on a host system that supports WireGuard or using the `linuxserver/wireguard` Docker image.
|
||||||
|
|
||||||
|
The recommended method for deploying WireGuard Portal is via Docker Compose for ease of configuration and management.
|
||||||
|
|
||||||
|
A sample docker-compose.yml (managing WireGuard interfaces directly on the host) is provided below:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
--8<-- "docker-compose.yml::17"
|
--8<-- "docker-compose.yml::19"
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, the webserver is listening on port **8888**.
|
By default, the webserver for the UI is listening on port **8888** on all available interfaces.
|
||||||
|
|
||||||
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
|
Volumes for `/app/data` and `/app/config` should be used ensure data persistence across container restarts.
|
||||||
|
|
||||||
|
## WireGuard Interface Handling
|
||||||
|
|
||||||
|
WireGuard Portal supports managing WireGuard interfaces through three distinct deployment methods, providing flexibility based on your system architecture and operational preferences:
|
||||||
|
|
||||||
|
- **Directly on the host system**:
|
||||||
|
WireGuard Portal can control WireGuard interfaces natively on the host, without using containers.
|
||||||
|
This setup is ideal for environments where direct access to system networking is preferred.
|
||||||
|
To use this method, you need to set the network mode to `host` in your docker-compose.yml file.
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wg-portal:
|
||||||
|
...
|
||||||
|
network_mode: "host"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
> :warning: If host networking is used, the WireGuard Portal UI will be accessible on all the host's IP addresses if the listening address is set to `:8888` in the configuration file.
|
||||||
|
To avoid this, you can bind the listening address to a specific IP address, for example, the loopback address (`127.0.0.1:8888`). It is also possible to deploy firewall rules to restrict access to the WireGuard Portal UI.
|
||||||
|
|
||||||
|
- **Within the WireGuard Portal Docker container**:
|
||||||
|
WireGuard interfaces can be managed directly from within the WireGuard Portal container itself.
|
||||||
|
This is the recommended approach when running WireGuard Portal via Docker, as it encapsulates all functionality in a single, portable container without requiring a separate WireGuard host or image.
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wg-portal:
|
||||||
|
image: wgportal/wg-portal:v2
|
||||||
|
container_name: wg-portal
|
||||||
|
...
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
# host port : container port
|
||||||
|
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||||
|
- "51820:51820/udp"
|
||||||
|
# Web UI port
|
||||||
|
- "8888:8888/tcp"
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
volumes:
|
||||||
|
# host path : container path
|
||||||
|
- ./wg/data:/app/data
|
||||||
|
- ./wg/config:/app/config
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Via a separate Docker container**:
|
||||||
|
WireGuard Portal can interface with and control WireGuard running in another Docker container, such as the [linuxserver/wireguard](https://docs.linuxserver.io/images/docker-wireguard/) image.
|
||||||
|
This method is useful in setups that already use `linuxserver/wireguard` or where you want to isolate the VPN backend from the portal frontend.
|
||||||
|
For this, you need to set the network mode to `service:wireguard` in your docker-compose.yml file, `wireguard` is the service name of your WireGuard container.
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
wg-portal:
|
||||||
|
image: wgportal/wg-portal:v2
|
||||||
|
container_name: wg-portal
|
||||||
|
...
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
network_mode: "service:wireguard" # So we ensure to stay on the same network as the wireguard container.
|
||||||
|
volumes:
|
||||||
|
# host path : container path
|
||||||
|
- ./wg/etc:/etc/wireguard
|
||||||
|
- ./wg/data:/app/data
|
||||||
|
- ./wg/config:/app/config
|
||||||
|
|
||||||
|
wireguard:
|
||||||
|
image: lscr.io/linuxserver/wireguard:latest
|
||||||
|
container_name: wireguard
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
# host port : container port
|
||||||
|
- "51820:51820/udp" # WireGuard port, needs to match the port in wg-portal interface config
|
||||||
|
- "8888:8888/tcp" # Noticed that the port of the web UI is exposed in the wireguard container.
|
||||||
|
volumes:
|
||||||
|
- ./wg/etc:/config/wg_confs # We share the configuration (wgx.conf) between wg-portal and wireguard
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
```
|
||||||
|
As the `linuxserver/wireguard` image uses _wg-quick_ to manage the interfaces, you need to have at least the following configuration set for WireGuard Portal:
|
||||||
|
```yaml
|
||||||
|
core:
|
||||||
|
# The WireGuard container uses wg-quick to manage the WireGuard interfaces - this conflicts with WireGuard Portal during startup.
|
||||||
|
# To avoid this, we need to set the restore_state option to false so that wg-quick can create the interfaces.
|
||||||
|
restore_state: false
|
||||||
|
# Usually, there are no existing interfaces in the WireGuard container, so we can set this to false.
|
||||||
|
import_existing: false
|
||||||
|
advanced:
|
||||||
|
# WireGuard Portal needs to export the WireGuard configuration as wg-quick config files so that the WireGuard container can use them.
|
||||||
|
config_storage_path: /etc/wireguard/
|
||||||
|
```
|
||||||
|
|
||||||
## Image Versioning
|
## 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).
|
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
|
||||||
|
|
||||||
|
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
|
||||||
|
|
||||||
There are three types of tags in the repository:
|
There are three types of tags in the repository:
|
||||||
|
|
||||||
#### Semantic versioned tags
|
#### Semantic versioned tags
|
||||||
|
|
||||||
For example, `1.0.19`.
|
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
|
||||||
|
|
||||||
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).
|
These are official releases of WireGuard Portal. For production deployments of WireGuard Portal, we strongly recommend using one of these versioned tags instead of the latest or canary tags.
|
||||||
|
|
||||||
Once these tags show up in this repository, they will never change.
|
There are different types of these tags:
|
||||||
|
|
||||||
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.
|
- Major version tags: `v2` or `2`. These tags always refer to the latest image for WireGuard Portal version **2**.
|
||||||
|
- Minor version tags: `v2.x` or `2.0`. These tags always refer to the latest image for WireGuard Portal version **2.x**.
|
||||||
|
- Specific version tags (patch version): `v2.0.0` or `2.0.0`. These tags denote a very specific release. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases). Once these tags for a specific version show up in the Docker repository, they will never change.
|
||||||
|
|
||||||
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`.
|
#### The `latest` tag
|
||||||
|
|
||||||
Version **1** is currently **stable**, version **2** is in **development**.
|
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
|
||||||
|
|
||||||
#### latest
|
#### The `master` tag
|
||||||
|
|
||||||
This is the most recent build to master! It changes a lot and is very unstable.
|
This is the most recent build to the main branch! It changes a lot and is very unstable.
|
||||||
|
|
||||||
We recommend that you don't use it except for development purposes.
|
We recommend that you don't use it except for development purposes or to test the latest features.
|
||||||
|
|
||||||
#### 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
|
## Configuration
|
||||||
|
|
||||||
You can configure WireGuard Portal using a yaml configuration file.
|
You can configure WireGuard Portal using a YAML configuration file.
|
||||||
The filepath of the yaml configuration file defaults to `/app/config/config.yml`.
|
The filepath of the YAML configuration file defaults to `/app/config/config.yaml`.
|
||||||
It is possible to override the configuration filepath using the environment variable **WG_PORTAL_CONFIG**.
|
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`.
|
By default, WireGuard Portal uses an SQLite database. The database is stored in `/app/data/sqlite.db`.
|
||||||
|
|
||||||
You should mount those directories as a volume:
|
You should mount those directories as a volume:
|
||||||
|
|
||||||
- /app/data
|
- `/app/data`
|
||||||
- /app/config
|
- `/app/config`
|
||||||
|
|
||||||
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
A detailed description of the configuration options can be found [here](../configuration/overview.md).
|
||||||
|
|
||||||
|
If you want to access configuration files in wg-quick format, you can mount the `/etc/wireguard` directory inside the container to a location of your choice.
|
||||||
|
Also enable the `config_storage_path` option in the configuration file:
|
||||||
|
```yaml
|
||||||
|
advanced:
|
||||||
|
config_storage_path: /etc/wireguard
|
||||||
|
```
|
||||||
|
98
docs/documentation/getting-started/reverse-proxy.md
Normal file
98
docs/documentation/getting-started/reverse-proxy.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
## Reverse Proxy for HTTPS
|
||||||
|
|
||||||
|
For production deployments, always serve the WireGuard Portal over HTTPS. You have two options to secure your connection:
|
||||||
|
|
||||||
|
|
||||||
|
### Reverse Proxy
|
||||||
|
|
||||||
|
Let a front‐end proxy handle HTTPS for you. This also frees you from managing certificates manually and is therefore the preferred option.
|
||||||
|
You can use Nginx, Traefik, Caddy or any other proxy.
|
||||||
|
|
||||||
|
Below is an example using a Docker Compose stack with [Traefik](https://traefik.io/traefik/).
|
||||||
|
It exposes the WireGuard Portal on `https://wg.domain.com` and redirects initial HTTP traffic to HTTPS.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
reverse-proxy:
|
||||||
|
image: traefik:v3.3
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
#- '--log.level=DEBUG'
|
||||||
|
- '--providers.docker.endpoint=unix:///var/run/docker.sock'
|
||||||
|
- '--providers.docker.exposedbydefault=false'
|
||||||
|
- '--entrypoints.web.address=:80'
|
||||||
|
- '--entrypoints.websecure.address=:443'
|
||||||
|
- '--entrypoints.websecure.http3'
|
||||||
|
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge=true'
|
||||||
|
- '--certificatesresolvers.letsencryptresolver.acme.httpchallenge.entrypoint=web'
|
||||||
|
- '--certificatesresolvers.letsencryptresolver.acme.email=your.email@domain.com'
|
||||||
|
- '--certificatesresolvers.letsencryptresolver.acme.storage=/letsencrypt/acme.json'
|
||||||
|
#- '--certificatesresolvers.letsencryptresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory' # just for testing
|
||||||
|
ports:
|
||||||
|
- 80:80 # for HTTP
|
||||||
|
- 443:443/tcp # for HTTPS
|
||||||
|
- 443:443/udp # for HTTP/3
|
||||||
|
volumes:
|
||||||
|
- acme-certs:/letsencrypt
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
# HTTP Catchall for redirecting HTTP -> HTTPS
|
||||||
|
- 'traefik.http.routers.dashboard-catchall.rule=Host(`wg.domain.com`) && PathPrefix(`/`)'
|
||||||
|
- 'traefik.http.routers.dashboard-catchall.entrypoints=web'
|
||||||
|
- 'traefik.http.routers.dashboard-catchall.middlewares=redirect-to-https'
|
||||||
|
- 'traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'
|
||||||
|
|
||||||
|
wg-portal:
|
||||||
|
image: wgportal/wg-portal:v2
|
||||||
|
container_name: wg-portal
|
||||||
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
# host port : container port
|
||||||
|
# WireGuard port, needs to match the port in wg-portal interface config (add one port mapping for each interface)
|
||||||
|
- "51820:51820/udp"
|
||||||
|
# Web UI port (only available on localhost, Traefik will handle the HTTPS)
|
||||||
|
- "127.0.0.1:8888:8888/tcp"
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.conf.all.src_valid_mark=1
|
||||||
|
volumes:
|
||||||
|
# host path : container path
|
||||||
|
- ./wg/data:/app/data
|
||||||
|
- ./wg/config:/app/config
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.wgportal.rule=Host(`wg.domain.com`)'
|
||||||
|
- 'traefik.http.routers.wgportal.entrypoints=websecure'
|
||||||
|
- 'traefik.http.routers.wgportal.tls.certresolver=letsencryptresolver'
|
||||||
|
- 'traefik.http.routers.wgportal.service=wgportal'
|
||||||
|
- 'traefik.http.services.wgportal.loadbalancer.server.port=8888'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
acme-certs:
|
||||||
|
```
|
||||||
|
|
||||||
|
The WireGuard Portal configuration must be updated accordingly so that the correct external URL is set for the web interface:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
external_url: https://wg.domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Built-in TLS
|
||||||
|
|
||||||
|
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||||
|
In your `config.yaml`, under the `web` section, point to your certificate and key files:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
cert_file: /path/to/your/fullchain.pem
|
||||||
|
key_file: /path/to/your/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
The web server will then use these files to serve HTTPS traffic directly instead of HTTP.
|
@ -4,8 +4,8 @@ To build the application from source files, use the Makefile provided in the rep
|
|||||||
|
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [Make](https://www.gnu.org/software/make/)
|
- [Make](https://www.gnu.org/software/make/)
|
||||||
- [Go](https://go.dev/dl/): `>=1.23.0`
|
- [Go](https://go.dev/dl/): `>=1.24.0`
|
||||||
- [NodeJS with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
|
- [Node.js with npm](https://nodejs.org/en/download): `node>=18, npm>=9`
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@ -22,3 +22,5 @@ make build
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
Compiled binary will be available in `./dist` directory.
|
Compiled binary will be available in `./dist` directory.
|
||||||
|
|
||||||
|
For installation instructions, check the [Binaries](./binaries.md) section.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
By default WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
|
By default, WG-Portal exposes Prometheus metrics on port `8787` if interface/peer statistic data collection is enabled.
|
||||||
|
|
||||||
## Exposed Metrics
|
## Exposed Metrics
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ By default WG-Portal exposes Prometheus metrics on port `8787` if interface/peer
|
|||||||
|
|
||||||
## Prometheus Config
|
## Prometheus Config
|
||||||
|
|
||||||
Add following scrape job to your Prometheus config file:
|
Add the following scrape job to your Prometheus config file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# prometheus.yaml
|
# prometheus.yaml
|
||||||
|
@ -1 +1 @@
|
|||||||
--8<-- "README.md:20:47"
|
--8<-- "README.md:12:41"
|
||||||
|
@ -42,24 +42,17 @@ definitions:
|
|||||||
description: Error message.
|
description: Error message.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
models.ExpiryDate:
|
|
||||||
properties:
|
|
||||||
time.Time:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
models.Interface:
|
models.Interface:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
description: Addresses is a list of IP addresses (in CIDR format) that are
|
description: Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
|
||||||
assigned to the interface.
|
|
||||||
example:
|
example:
|
||||||
- 10.11.12.1/24
|
- 10.11.12.1/24
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
Disabled:
|
Disabled:
|
||||||
description: Disabled is a flag that specifies if the interface is enabled
|
description: Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
|
||||||
(up) or not (down). Disabled interfaces are not able to accept connections.
|
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
DisabledReason:
|
DisabledReason:
|
||||||
@ -72,45 +65,46 @@ definitions:
|
|||||||
maxLength: 64
|
maxLength: 64
|
||||||
type: string
|
type: string
|
||||||
Dns:
|
Dns:
|
||||||
description: Dns is a list of DNS servers that should be set if the interface
|
description: Dns is a list of DNS servers that should be set if the interface is up.
|
||||||
is up.
|
|
||||||
example:
|
example:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
DnsSearch:
|
DnsSearch:
|
||||||
description: DnsSearch is the dns search option string that should be set
|
description: DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
|
||||||
if the interface is up, will be appended to Dns servers.
|
|
||||||
example:
|
example:
|
||||||
- wg.local
|
- wg.local
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
EnabledPeers:
|
EnabledPeers:
|
||||||
description: EnabledPeers is the number of enabled peers for this interface.
|
description: EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
|
||||||
Only enabled peers are able to connect.
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
type: integer
|
type: integer
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this interface.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg0.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
description: FirewallMark is an optional firewall mark which is used to handle
|
description: FirewallMark is an optional firewall mark which is used to handle interface traffic.
|
||||||
interface traffic.
|
|
||||||
type: integer
|
type: integer
|
||||||
Identifier:
|
Identifier:
|
||||||
description: Identifier is the unique identifier of the interface. It is always
|
description: Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
|
||||||
equal to the device name of the interface.
|
|
||||||
example: wg0
|
example: wg0
|
||||||
type: string
|
type: string
|
||||||
ListenPort:
|
ListenPort:
|
||||||
description: 'ListenPort is the listening port, for example: 51820. The listening
|
description: 'ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.'
|
||||||
port is only required for server interfaces.'
|
|
||||||
example: 51820
|
example: 51820
|
||||||
maximum: 65535
|
maximum: 65535
|
||||||
minimum: 1
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
Mode:
|
Mode:
|
||||||
description: Mode is the interface type, either 'server', 'client' or 'any'.
|
description: Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
|
||||||
The mode specifies how WireGuard Portal handles peers for this interface.
|
|
||||||
enum:
|
enum:
|
||||||
- server
|
- server
|
||||||
- client
|
- client
|
||||||
@ -124,8 +118,7 @@ definitions:
|
|||||||
minimum: 1
|
minimum: 1
|
||||||
type: integer
|
type: integer
|
||||||
PeerDefAllowedIPs:
|
PeerDefAllowedIPs:
|
||||||
description: PeerDefAllowedIPs specifies the default allowed IP addresses
|
description: PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
|
||||||
for a new peer.
|
|
||||||
example:
|
example:
|
||||||
- 10.11.12.0/24
|
- 10.11.12.0/24
|
||||||
items:
|
items:
|
||||||
@ -139,8 +132,7 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
PeerDefDnsSearch:
|
PeerDefDnsSearch:
|
||||||
description: PeerDefDnsSearch specifies the default dns search options for
|
description: PeerDefDnsSearch specifies the default dns search options for a new peer.
|
||||||
a new peer.
|
|
||||||
example:
|
example:
|
||||||
- wg.local
|
- wg.local
|
||||||
items:
|
items:
|
||||||
@ -151,64 +143,52 @@ definitions:
|
|||||||
example: wg.example.com:51820
|
example: wg.example.com:51820
|
||||||
type: string
|
type: string
|
||||||
PeerDefFirewallMark:
|
PeerDefFirewallMark:
|
||||||
description: PeerDefFirewallMark specifies the default firewall mark for a
|
description: PeerDefFirewallMark specifies the default firewall mark for a new peer.
|
||||||
new peer.
|
|
||||||
type: integer
|
type: integer
|
||||||
PeerDefMtu:
|
PeerDefMtu:
|
||||||
description: PeerDefMtu specifies the default device MTU for a new peer.
|
description: PeerDefMtu specifies the default device MTU for a new peer.
|
||||||
example: 1420
|
example: 1420
|
||||||
type: integer
|
type: integer
|
||||||
PeerDefNetwork:
|
PeerDefNetwork:
|
||||||
description: PeerDefNetwork specifies the default subnets from which new peers
|
description: PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
|
||||||
will get their IP addresses. The subnet is specified in CIDR format.
|
|
||||||
example:
|
example:
|
||||||
- 10.11.12.0/24
|
- 10.11.12.0/24
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
PeerDefPersistentKeepalive:
|
PeerDefPersistentKeepalive:
|
||||||
description: PeerDefPersistentKeepalive specifies the default persistent keep-alive
|
description: PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
|
||||||
value in seconds for a new peer.
|
|
||||||
example: 25
|
example: 25
|
||||||
type: integer
|
type: integer
|
||||||
PeerDefPostDown:
|
PeerDefPostDown:
|
||||||
description: PeerDefPostDown specifies the default action that is executed
|
description: PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
|
||||||
after the device is down for a new peer.
|
|
||||||
type: string
|
type: string
|
||||||
PeerDefPostUp:
|
PeerDefPostUp:
|
||||||
description: PeerDefPostUp specifies the default action that is executed after
|
description: PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
|
||||||
the device is up for a new peer.
|
|
||||||
type: string
|
type: string
|
||||||
PeerDefPreDown:
|
PeerDefPreDown:
|
||||||
description: PeerDefPreDown specifies the default action that is executed
|
description: PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
|
||||||
before the device is down for a new peer.
|
|
||||||
type: string
|
type: string
|
||||||
PeerDefPreUp:
|
PeerDefPreUp:
|
||||||
description: PeerDefPreUp specifies the default action that is executed before
|
description: PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
|
||||||
the device is up for a new peer.
|
|
||||||
type: string
|
type: string
|
||||||
PeerDefRoutingTable:
|
PeerDefRoutingTable:
|
||||||
description: PeerDefRoutingTable specifies the default routing table for a
|
description: PeerDefRoutingTable specifies the default routing table for a new peer.
|
||||||
new peer.
|
|
||||||
type: string
|
type: string
|
||||||
PostDown:
|
PostDown:
|
||||||
description: PostDown is an optional action that is executed after the device
|
description: PostDown is an optional action that is executed after the device is down.
|
||||||
is down.
|
|
||||||
example: echo 'Interface is down'
|
example: echo 'Interface is down'
|
||||||
type: string
|
type: string
|
||||||
PostUp:
|
PostUp:
|
||||||
description: PostUp is an optional action that is executed after the device
|
description: PostUp is an optional action that is executed after the device is up.
|
||||||
is up.
|
|
||||||
example: iptables -A FORWARD -i %i -j ACCEPT
|
example: iptables -A FORWARD -i %i -j ACCEPT
|
||||||
type: string
|
type: string
|
||||||
PreDown:
|
PreDown:
|
||||||
description: PreDown is an optional action that is executed before the device
|
description: PreDown is an optional action that is executed before the device is down.
|
||||||
is down.
|
|
||||||
example: iptables -D FORWARD -i %i -j ACCEPT
|
example: iptables -D FORWARD -i %i -j ACCEPT
|
||||||
type: string
|
type: string
|
||||||
PreUp:
|
PreUp:
|
||||||
description: PreUp is an optional action that is executed before the device
|
description: PreUp is an optional action that is executed before the device is up.
|
||||||
is up.
|
|
||||||
example: echo 'Interface is up'
|
example: echo 'Interface is up'
|
||||||
type: string
|
type: string
|
||||||
PrivateKey:
|
PrivateKey:
|
||||||
@ -216,17 +196,14 @@ definitions:
|
|||||||
example: gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=
|
example: gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=
|
||||||
type: string
|
type: string
|
||||||
PublicKey:
|
PublicKey:
|
||||||
description: PublicKey is the public key of the server interface. The public
|
description: PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
|
||||||
key is used by peers to connect to the server.
|
|
||||||
example: HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=
|
example: HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=
|
||||||
type: string
|
type: string
|
||||||
RoutingTable:
|
RoutingTable:
|
||||||
description: RoutingTable is an optional routing table which is used to route
|
description: RoutingTable is an optional routing table which is used to route interface traffic.
|
||||||
interface traffic.
|
|
||||||
type: string
|
type: string
|
||||||
SaveConfig:
|
SaveConfig:
|
||||||
description: SaveConfig is a flag that specifies if the configuration should
|
description: SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
|
||||||
be saved to the configuration file (wgX.conf in wg-quick format).
|
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
TotalPeers:
|
TotalPeers:
|
||||||
@ -257,8 +234,7 @@ definitions:
|
|||||||
models.Peer:
|
models.Peer:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
description: Addresses is a list of IP addresses in CIDR format (both IPv4
|
description: Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
|
||||||
and IPv6) for the peer.
|
|
||||||
example:
|
example:
|
||||||
- 10.11.12.2/24
|
- 10.11.12.2/24
|
||||||
items:
|
items:
|
||||||
@ -269,13 +245,11 @@ definitions:
|
|||||||
- $ref: '#/definitions/models.ConfigOption-array_string'
|
- $ref: '#/definitions/models.ConfigOption-array_string'
|
||||||
description: AllowedIPs is a list of allowed IP subnets for the peer.
|
description: AllowedIPs is a list of allowed IP subnets for the peer.
|
||||||
CheckAliveAddress:
|
CheckAliveAddress:
|
||||||
description: CheckAliveAddress is an optional ip address or DNS name that
|
description: CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
|
||||||
is used for ping checks.
|
|
||||||
example: 1.1.1.1
|
example: 1.1.1.1
|
||||||
type: string
|
type: string
|
||||||
Disabled:
|
Disabled:
|
||||||
description: Disabled is a flag that specifies if the peer is enabled or not.
|
description: Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
Disabled peers are not able to connect.
|
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
DisabledReason:
|
DisabledReason:
|
||||||
@ -290,13 +264,11 @@ definitions:
|
|||||||
Dns:
|
Dns:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-array_string'
|
- $ref: '#/definitions/models.ConfigOption-array_string'
|
||||||
description: Dns is a list of DNS servers that should be set if the peer interface
|
description: Dns is a list of DNS servers that should be set if the peer interface is up.
|
||||||
is up.
|
|
||||||
DnsSearch:
|
DnsSearch:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-array_string'
|
- $ref: '#/definitions/models.ConfigOption-array_string'
|
||||||
description: DnsSearch is the dns search option string that should be set
|
description: DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
|
||||||
if the peer interface is up, will be appended to Dns servers.
|
|
||||||
Endpoint:
|
Endpoint:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
@ -306,29 +278,31 @@ definitions:
|
|||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: EndpointPublicKey is the endpoint public key.
|
description: EndpointPublicKey is the endpoint public key.
|
||||||
ExpiresAt:
|
ExpiresAt:
|
||||||
allOf:
|
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
|
||||||
- $ref: '#/definitions/models.ExpiryDate'
|
type: string
|
||||||
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format.
|
|
||||||
An expired peer is not able to connect.
|
|
||||||
ExtraAllowedIPs:
|
ExtraAllowedIPs:
|
||||||
description: ExtraAllowedIPs is a list of additional allowed IP subnets for
|
description: ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
|
||||||
the peer. These allowed IP subnets are added on the server side.
|
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this peer.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg_peer_x.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-uint32'
|
- $ref: '#/definitions/models.ConfigOption-uint32'
|
||||||
description: FirewallMark is an optional firewall mark which is used to handle
|
description: FirewallMark is an optional firewall mark which is used to handle peer traffic.
|
||||||
peer traffic.
|
|
||||||
Identifier:
|
Identifier:
|
||||||
description: Identifier is the unique identifier of the peer. It is always
|
description: Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
|
||||||
equal to the public key of the peer.
|
|
||||||
example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
|
example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
|
||||||
type: string
|
type: string
|
||||||
InterfaceIdentifier:
|
InterfaceIdentifier:
|
||||||
description: InterfaceIdentifier is the identifier of the interface the peer
|
description: InterfaceIdentifier is the identifier of the interface the peer is linked to.
|
||||||
is linked to.
|
|
||||||
example: wg0
|
example: wg0
|
||||||
type: string
|
type: string
|
||||||
Mode:
|
Mode:
|
||||||
@ -350,28 +324,23 @@ definitions:
|
|||||||
PersistentKeepalive:
|
PersistentKeepalive:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-int'
|
- $ref: '#/definitions/models.ConfigOption-int'
|
||||||
description: PersistentKeepalive is the optional persistent keep-alive interval
|
description: PersistentKeepalive is the optional persistent keep-alive interval in seconds.
|
||||||
in seconds.
|
|
||||||
PostDown:
|
PostDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: PostDown is an optional action that is executed after the device
|
description: PostDown is an optional action that is executed after the device is down.
|
||||||
is down.
|
|
||||||
PostUp:
|
PostUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: PostUp is an optional action that is executed after the device
|
description: PostUp is an optional action that is executed after the device is up.
|
||||||
is up.
|
|
||||||
PreDown:
|
PreDown:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: PreDown is an optional action that is executed before the device
|
description: PreDown is an optional action that is executed before the device is down.
|
||||||
is down.
|
|
||||||
PreUp:
|
PreUp:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: PreUp is an optional action that is executed before the device
|
description: PreUp is an optional action that is executed before the device is up.
|
||||||
is up.
|
|
||||||
PresharedKey:
|
PresharedKey:
|
||||||
description: PresharedKey is the optional pre-shared Key of the peer.
|
description: PresharedKey is the optional pre-shared Key of the peer.
|
||||||
example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
|
example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
|
||||||
@ -387,8 +356,7 @@ definitions:
|
|||||||
RoutingTable:
|
RoutingTable:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: RoutingTable is an optional routing table which is used to route
|
description: RoutingTable is an optional routing table which is used to route peer traffic.
|
||||||
peer traffic.
|
|
||||||
UserIdentifier:
|
UserIdentifier:
|
||||||
description: UserIdentifier is the identifier of the user that owns the peer.
|
description: UserIdentifier is the identifier of the user that owns the peer.
|
||||||
example: uid-1234567
|
example: uid-1234567
|
||||||
@ -436,18 +404,15 @@ definitions:
|
|||||||
models.ProvisioningRequest:
|
models.ProvisioningRequest:
|
||||||
properties:
|
properties:
|
||||||
InterfaceIdentifier:
|
InterfaceIdentifier:
|
||||||
description: InterfaceIdentifier is the identifier of the WireGuard interface
|
description: InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
|
||||||
the peer should be linked to.
|
|
||||||
example: wg0
|
example: wg0
|
||||||
type: string
|
type: string
|
||||||
PresharedKey:
|
PresharedKey:
|
||||||
description: PresharedKey is the optional pre-shared key of the peer. If no
|
description: PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
|
||||||
pre-shared key is set, a new key is generated.
|
|
||||||
example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
|
example: yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=
|
||||||
type: string
|
type: string
|
||||||
PublicKey:
|
PublicKey:
|
||||||
description: PublicKey is the optional public key of the peer. If no public
|
description: PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
|
||||||
key is set, a new key pair is generated.
|
|
||||||
example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
|
example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
|
||||||
type: string
|
type: string
|
||||||
UserIdentifier:
|
UserIdentifier:
|
||||||
@ -462,14 +427,12 @@ definitions:
|
|||||||
models.User:
|
models.User:
|
||||||
properties:
|
properties:
|
||||||
ApiEnabled:
|
ApiEnabled:
|
||||||
description: If this field is set, the user is allowed to use the RESTful
|
description: If this field is set, the user is allowed to use the RESTful API. This field is read-only.
|
||||||
API. This field is read-only.
|
|
||||||
example: false
|
example: false
|
||||||
readOnly: true
|
readOnly: true
|
||||||
type: boolean
|
type: boolean
|
||||||
ApiToken:
|
ApiToken:
|
||||||
description: The API token of the user. This field is never populated on bulk
|
description: The API token of the user. This field is never populated on bulk read operations.
|
||||||
read operations.
|
|
||||||
example: ""
|
example: ""
|
||||||
maxLength: 64
|
maxLength: 64
|
||||||
minLength: 32
|
minLength: 32
|
||||||
@ -508,8 +471,7 @@ definitions:
|
|||||||
example: Muster
|
example: Muster
|
||||||
type: string
|
type: string
|
||||||
Locked:
|
Locked:
|
||||||
description: If this field is set, the user is locked and thus unable to log
|
description: If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
|
||||||
in to WireGuard Portal.
|
|
||||||
example: false
|
example: false
|
||||||
type: boolean
|
type: boolean
|
||||||
LockedReason:
|
LockedReason:
|
||||||
@ -521,8 +483,7 @@ definitions:
|
|||||||
example: some sample notes
|
example: some sample notes
|
||||||
type: string
|
type: string
|
||||||
Password:
|
Password:
|
||||||
description: The password of the user. This field is never populated on read
|
description: The password of the user. This field is never populated on read operations.
|
||||||
operations.
|
|
||||||
example: ""
|
example: ""
|
||||||
maxLength: 64
|
maxLength: 64
|
||||||
minLength: 16
|
minLength: 16
|
||||||
@ -549,7 +510,6 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- Identifier
|
- Identifier
|
||||||
- IsAdmin
|
|
||||||
type: object
|
type: object
|
||||||
models.UserInformation:
|
models.UserInformation:
|
||||||
properties:
|
properties:
|
||||||
@ -574,39 +534,33 @@ definitions:
|
|||||||
example: My iPhone
|
example: My iPhone
|
||||||
type: string
|
type: string
|
||||||
Identifier:
|
Identifier:
|
||||||
description: Identifier is the unique identifier of the peer. It equals the
|
description: Identifier is the unique identifier of the peer. It equals the public key of the peer.
|
||||||
public key of the peer.
|
|
||||||
example: peer-1234567
|
example: peer-1234567
|
||||||
type: string
|
type: string
|
||||||
InterfaceIdentifier:
|
InterfaceIdentifier:
|
||||||
description: InterfaceIdentifier is the unique identifier of the WireGuard
|
description: InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
|
||||||
Portal device the peer is connected to.
|
|
||||||
example: wg0
|
example: wg0
|
||||||
type: string
|
type: string
|
||||||
IpAddresses:
|
IpAddresses:
|
||||||
description: IPAddresses is a list of IP addresses in CIDR format assigned
|
description: IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
|
||||||
to the peer.
|
|
||||||
example:
|
example:
|
||||||
- 10.11.12.2/24
|
- 10.11.12.2/24
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
IsDisabled:
|
IsDisabled:
|
||||||
description: IsDisabled is a flag that specifies if the peer is enabled or
|
description: IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
|
||||||
not. Disabled peers are not able to connect.
|
|
||||||
example: true
|
example: true
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
models.UserMetrics:
|
models.UserMetrics:
|
||||||
properties:
|
properties:
|
||||||
BytesReceived:
|
BytesReceived:
|
||||||
description: The total number of bytes received by the user. This is the sum
|
description: The total number of bytes received by the user. This is the sum of all bytes received by the peers linked to the user.
|
||||||
of all bytes received by the peers linked to the user.
|
|
||||||
example: 123456789
|
example: 123456789
|
||||||
type: integer
|
type: integer
|
||||||
BytesTransmitted:
|
BytesTransmitted:
|
||||||
description: The total number of bytes transmitted by the user. This is the
|
description: The total number of bytes transmitted by the user. This is the sum of all bytes transmitted by the peers linked to the user.
|
||||||
sum of all bytes transmitted by the peers linked to the user.
|
|
||||||
example: 123456789
|
example: 123456789
|
||||||
type: integer
|
type: integer
|
||||||
PeerCount:
|
PeerCount:
|
||||||
@ -614,8 +568,7 @@ definitions:
|
|||||||
example: 2
|
example: 2
|
||||||
type: integer
|
type: integer
|
||||||
PeerMetrics:
|
PeerMetrics:
|
||||||
description: PeerMetrics represents the metrics of the peers linked to the
|
description: PeerMetrics represents the metrics of the peers linked to the user.
|
||||||
user.
|
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/models.PeerMetrics'
|
$ref: '#/definitions/models.PeerMetrics'
|
||||||
type: array
|
type: array
|
||||||
@ -739,6 +692,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
put:
|
put:
|
||||||
|
description: This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleUpdatePut
|
operationId: interfaces_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface identifier.
|
- description: The interface identifier.
|
||||||
@ -786,6 +740,7 @@ paths:
|
|||||||
- Interfaces
|
- Interfaces
|
||||||
/interface/new:
|
/interface/new:
|
||||||
post:
|
post:
|
||||||
|
description: This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleCreatePost
|
operationId: interfaces_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface data.
|
- description: The interface data.
|
||||||
@ -826,6 +781,34 @@ paths:
|
|||||||
summary: Create a new interface record.
|
summary: Create a new interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
|
/interface/prepare:
|
||||||
|
get:
|
||||||
|
description: This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
|
||||||
|
operationId: interfaces_handlePrepareGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Interface'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new interface record.
|
||||||
|
tags:
|
||||||
|
- Interfaces
|
||||||
/metrics/by-interface/{id}:
|
/metrics/by-interface/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: metrics_handleMetricsForInterfaceGet
|
operationId: metrics_handleMetricsForInterfaceGet
|
||||||
@ -977,8 +960,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own records. Admins can access
|
description: Normal users can only access their own records. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: peers_handleByIdGet
|
operationId: peers_handleByIdGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier (public key).
|
- description: The peer identifier (public key).
|
||||||
@ -1015,7 +997,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
put:
|
put:
|
||||||
description: Only admins can update existing records.
|
description: Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleUpdatePut
|
operationId: peers_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier.
|
- description: The peer identifier.
|
||||||
@ -1094,8 +1076,7 @@ paths:
|
|||||||
- Peers
|
- Peers
|
||||||
/peer/by-user/{id}:
|
/peer/by-user/{id}:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own records. Admins can access
|
description: Normal users can only access their own records. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: peers_handleAllForUserGet
|
operationId: peers_handleAllForUserGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The user identifier.
|
- description: The user identifier.
|
||||||
@ -1127,7 +1108,7 @@ paths:
|
|||||||
- Peers
|
- Peers
|
||||||
/peer/new:
|
/peer/new:
|
||||||
post:
|
post:
|
||||||
description: Only admins can create new records.
|
description: Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer data.
|
- description: The peer data.
|
||||||
@ -1168,10 +1149,51 @@ paths:
|
|||||||
summary: Create a new peer record.
|
summary: Create a new peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
|
/peer/prepare/{id}:
|
||||||
|
get:
|
||||||
|
description: This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
|
||||||
|
operationId: peers_handlePrepareGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Peer'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new peer record for the given WireGuard interface.
|
||||||
|
tags:
|
||||||
|
- Peers
|
||||||
/provisioning/data/peer-config:
|
/provisioning/data/peer-config:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: provisioning_handlePeerConfigGet
|
operationId: provisioning_handlePeerConfigGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier (public key) that should be queried.
|
- description: The peer identifier (public key) that should be queried.
|
||||||
@ -1214,8 +1236,7 @@ paths:
|
|||||||
- Provisioning
|
- Provisioning
|
||||||
/provisioning/data/peer-qr:
|
/provisioning/data/peer-qr:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: provisioning_handlePeerQrGet
|
operationId: provisioning_handlePeerQrGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier (public key) that should be queried.
|
- description: The peer identifier (public key) that should be queried.
|
||||||
@ -1258,17 +1279,14 @@ paths:
|
|||||||
- Provisioning
|
- Provisioning
|
||||||
/provisioning/data/user-info:
|
/provisioning/data/user-info:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: provisioning_handleUserInfoGet
|
operationId: provisioning_handleUserInfoGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The user identifier that should be queried. If not set, the authenticated
|
- description: The user identifier that should be queried. If not set, the authenticated user is used.
|
||||||
user is used.
|
|
||||||
in: query
|
in: query
|
||||||
name: UserId
|
name: UserId
|
||||||
type: string
|
type: string
|
||||||
- description: The email address that should be queried. If UserId is set, this
|
- description: The email address that should be queried. If UserId is set, this is ignored.
|
||||||
is ignored.
|
|
||||||
in: query
|
in: query
|
||||||
name: Email
|
name: Email
|
||||||
type: string
|
type: string
|
||||||
@ -1306,8 +1324,7 @@ paths:
|
|||||||
- Provisioning
|
- Provisioning
|
||||||
/provisioning/new-peer:
|
/provisioning/new-peer:
|
||||||
post:
|
post:
|
||||||
description: Normal users can only create new peers if self provisioning is
|
description: Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||||
allowed. Admins can always add new peers.
|
|
||||||
operationId: provisioning_handleNewPeerPost
|
operationId: provisioning_handleNewPeerPost
|
||||||
parameters:
|
parameters:
|
||||||
- description: Provisioning request model.
|
- description: Provisioning request model.
|
||||||
@ -1413,8 +1430,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access all records.
|
||||||
all records.
|
|
||||||
operationId: users_handleByIdGet
|
operationId: users_handleByIdGet
|
||||||
parameters:
|
parameters:
|
||||||
- description: The user identifier.
|
- description: The user identifier.
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
For production deployments of WireGuard Portal, we strongly recommend using version 1.
|
Major upgrades between different versions may require special procedures, which are described in the following sections.
|
||||||
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
|
## 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!
|
> :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.
|
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.
|
The configuration (config.yaml) for WireGuard Portal must be updated and valid before starting the upgrade.
|
||||||
|
|
||||||
To upgrade from a previous SQLite database, start wg-portal like:
|
To upgrade from a previous SQLite database, start wg-portal like:
|
||||||
|
|
||||||
@ -14,14 +13,16 @@ To upgrade from a previous SQLite database, start wg-portal like:
|
|||||||
./wg-portal-amd64 -migrateFrom=old_wg_portal.db
|
./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.
|
You can also specify the database type using the parameter **-migrateFromType**.
|
||||||
|
Supported database types: `mysql`, `mssql`, `postgres` or `sqlite`.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./wg-portal-amd64 -migrateFromType=mysql -migrateFrom='user:pass@tcp(1.2.3.4:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local'
|
./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.
|
The upgrade will transform the old, existing database and store the values in the new database specified in the **config.yaml** configuration file.
|
||||||
Ensure that the new database does not contain any data!
|
Ensure that the new database does not contain any data!
|
||||||
|
|
||||||
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
If you are using Docker, you can adapt the docker-compose.yml file to start the upgrade process:
|
||||||
@ -29,8 +30,8 @@ If you are using Docker, you can adapt the docker-compose.yml file to start the
|
|||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
wg-portal:
|
wg-portal:
|
||||||
image: wgportal/wg-portal:latest
|
image: wgportal/wg-portal:v2
|
||||||
# ... other settings
|
# ... other settings
|
||||||
restart: no
|
restart: no
|
||||||
command: ["-migrateFrom=/app/data/wg_portal.db"]
|
command: ["-migrateFrom=/app/data/old_wg_portal.db"]
|
||||||
```
|
```
|
||||||
|
57
docs/documentation/usage/general.md
Normal file
57
docs/documentation/usage/general.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
This documentation section describes the general usage of WireGuard Portal.
|
||||||
|
If you are looking for specific setup instructions, please refer to the *Getting Started* and [*Configuration*](../configuration/overview.md) sections,
|
||||||
|
for example, using a [Docker](../getting-started/docker.md) deployment.
|
||||||
|
|
||||||
|
## Basic Concepts
|
||||||
|
|
||||||
|
WireGuard Portal is a web-based configuration portal for WireGuard server management. It allows managing multiple WireGuard interfaces and users from a single web UI.
|
||||||
|
WireGuard Interfaces can be categorized into three types:
|
||||||
|
|
||||||
|
- **Server**: A WireGuard server interface that to which multiple peers can connect. In this mode, it is possible to specify default settings for all peers, such as the IP address range, DNS servers, and MTU size.
|
||||||
|
- **Client**: A WireGuard client interface that can be used to connect to a WireGuard server. Usually, such an interface has exactly one peer.
|
||||||
|
- **Unknown**: This is the default type for imported interfaces. It is encouraged to change the type to either `Server` or `Client` after importing the interface.
|
||||||
|
|
||||||
|
## Accessing the Web UI
|
||||||
|
|
||||||
|
The web UI should be accessed via the URL specified in the `external_url` property of the configuration file.
|
||||||
|
By default, WireGuard Portal listens on port `8888` for HTTP connections. Check the [Security](security.md) section for more information on securing the web UI.
|
||||||
|
|
||||||
|
So the default URL to access the web UI is:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
A freshly set-up WireGuard Portal instance will have a default admin user with the username `admin@wgportal.local` and the password `wgportal-default`.
|
||||||
|
You can and should override the default credentials in the configuration file. Make sure to change the default password immediately after the first login!
|
||||||
|
|
||||||
|
|
||||||
|
### Basic UI Description
|
||||||
|
|
||||||
|

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

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

|
||||||
|
|
||||||
|
|
||||||
|
### OAuth and OIDC Authentication
|
||||||
|
|
||||||
|
WireGuard Portal supports OAuth and OIDC authentication. You can use any OAuth or OIDC provider that supports the authorization code flow,
|
||||||
|
such as Google, GitHub, or Keycloak.
|
||||||
|
|
||||||
|
For OAuth or OIDC to work, you need to configure the [`external_url`](../configuration/overview.md#external_url) property in the [`web`](../configuration/overview.md#web) section of the configuration file.
|
||||||
|
If you are planning to expose the portal to the internet, make sure that the `external_url` is configured to use HTTPS.
|
||||||
|
|
||||||
|
To add OIDC or OAuth authentication to WireGuard Portal, create a Client-ID and Client-Secret in your OAuth provider and
|
||||||
|
configure a new authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
Make sure that each configured provider has a unique `provider_name` property set. Samples can be seen [here](../configuration/examples.md).
|
||||||
|
|
||||||
|
#### Limiting Login to Specific Domains
|
||||||
|
|
||||||
|
You can limit the login to specific domains by setting the `allowed_domains` property for OAuth or OIDC providers.
|
||||||
|
This property is a comma-separated list of domains that are allowed to log in. The user's email address is checked against this list.
|
||||||
|
For example, if you want to allow only users with an email address ending in `outlook.com` to log in, set the property as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
allowed_domains:
|
||||||
|
- "outlook.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Limit Login to Existing Users
|
||||||
|
|
||||||
|
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth or OIDC providers.
|
||||||
|
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||||
|
|
||||||
|
#### Admin Mapping
|
||||||
|
|
||||||
|
You can map users to admin roles based on their attributes in the OAuth or OIDC provider. To do this, set the `admin_mapping` property for the provider.
|
||||||
|
Administrative access can either be mapped by a specific attribute or by group membership.
|
||||||
|
|
||||||
|
**Attribute specific mapping** can be achieved by setting the `admin_value_regex` and the `is_admin` property.
|
||||||
|
The `admin_value_regex` property is a regular expression that is matched against the value of the `is_admin` attribute.
|
||||||
|
The user is granted admin access if the regex matches the attribute value.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
field_map:
|
||||||
|
is_admin: "wg_admin_prop"
|
||||||
|
admin_mapping:
|
||||||
|
admin_value_regex: "^true$"
|
||||||
|
```
|
||||||
|
The example above will grant admin access to users with the `wg_admin_prop` attribute set to `true`.
|
||||||
|
|
||||||
|
**Group membership mapping** can be achieved by setting the `admin_group_regex` and `user_groups` property.
|
||||||
|
The `admin_group_regex` property is a regular expression that is matched against the group names of the user.
|
||||||
|
The user is granted admin access if the regex matches any of the group names.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
oidc:
|
||||||
|
- provider_name: "oidc1"
|
||||||
|
# ... other settings
|
||||||
|
field_map:
|
||||||
|
user_groups: "groups"
|
||||||
|
admin_mapping:
|
||||||
|
admin_group_regex: "^the-admin-group$"
|
||||||
|
```
|
||||||
|
The example above will grant admin access to users who are members of the `the-admin-group` group.
|
||||||
|
|
||||||
|
|
||||||
|
### LDAP Authentication
|
||||||
|
|
||||||
|
WireGuard Portal supports LDAP authentication. You can use any LDAP server that supports the LDAP protocol, such as Active Directory or OpenLDAP.
|
||||||
|
Multiple LDAP servers can be configured in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
WireGuard Portal remembers the authentication provider of the user and therefore avoids conflicts between multiple LDAP providers.
|
||||||
|
|
||||||
|
To configure LDAP authentication, create a new [`ldap`](../configuration/overview.md#ldap) authentication provider in the [`auth`](../configuration/overview.md#auth) section of the configuration file.
|
||||||
|
|
||||||
|
#### Limiting Login to Specific Users
|
||||||
|
|
||||||
|
You can limit the login to specific users by setting the `login_filter` property for LDAP provider. This filter uses the LDAP search filter syntax.
|
||||||
|
The username can be inserted into the query by placing the `{{login_identifier}}` placeholder in the filter. This placeholder will then be replaced with the username entered by the user during login.
|
||||||
|
|
||||||
|
For example, if you want to allow only users with the `objectClass` attribute set to `organizationalPerson` to log in, set the property as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
ldap:
|
||||||
|
- provider_name: "ldap1"
|
||||||
|
# ... other settings
|
||||||
|
login_filter: "(&(objectClass=organizationalPerson)(uid={{login_identifier}}))"
|
||||||
|
```
|
||||||
|
|
||||||
|
The `login_filter` should always be designed to return at most one user.
|
||||||
|
|
||||||
|
#### Limit Login to Existing Users
|
||||||
|
|
||||||
|
You can limit the login to existing users only by setting the `registration_enabled` property to `false` for LDAP providers.
|
||||||
|
If registration is enabled, new users will be created in the database when they log in for the first time.
|
||||||
|
|
||||||
|
#### Admin Mapping
|
||||||
|
|
||||||
|
You can map users to admin roles based on their group membership in the LDAP server. To do this, set the `admin_group` and `memberof` property for the provider.
|
||||||
|
The `admin_group` property defines the distinguished name of the group that is allowed to log in as admin.
|
||||||
|
All groups that are listed in the `memberof` attribute of the user will be checked against this group. If one of the groups matches, the user is granted admin access.
|
||||||
|
|
||||||
|
|
||||||
|
## UI and API Access
|
||||||
|
|
||||||
|
WireGuard Portal provides a web UI and a REST API for user interaction. It is important to secure these interfaces to prevent unauthorized access and data breaches.
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
It is recommended to use HTTPS for all communication with the portal to prevent eavesdropping.
|
||||||
|
|
||||||
|
Event though, WireGuard Portal supports HTTPS out of the box, it is recommended to use a reverse proxy like Nginx or Traefik to handle SSL termination and other security features.
|
||||||
|
A detailed explanation is available in the [Reverse Proxy](../getting-started/reverse-proxy.md) section.
|
86
docs/documentation/usage/webhooks.md
Normal file
86
docs/documentation/usage/webhooks.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows.
|
||||||
|
|
||||||
|
When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.
|
||||||
|
The payload contains event-specific data in JSON format.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook).
|
||||||
|
|
||||||
|
A basic webhook configuration looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request.
|
||||||
|
You can set the shared secret in the webhook configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
webhook:
|
||||||
|
url: https://your-service.example.com/webhook
|
||||||
|
secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
|
||||||
|
```
|
||||||
|
|
||||||
|
You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering.
|
||||||
|
|
||||||
|
## Available Events
|
||||||
|
|
||||||
|
WireGuard Portal supports various events that can trigger webhooks. The following events are available:
|
||||||
|
|
||||||
|
- `create`: Triggered when a new entity is created.
|
||||||
|
- `update`: Triggered when an existing entity is updated.
|
||||||
|
- `delete`: Triggered when an entity is deleted.
|
||||||
|
- `connect`: Triggered when a user connects to the VPN.
|
||||||
|
- `disconnect`: Triggered when a user disconnects from the VPN.
|
||||||
|
|
||||||
|
The following entity types can trigger webhooks:
|
||||||
|
|
||||||
|
- `user`: When a WireGuard Portal user is created, updated, or deleted.
|
||||||
|
- `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events.
|
||||||
|
- `interface`: When a device is created, updated, or deleted.
|
||||||
|
|
||||||
|
## Payload Structure
|
||||||
|
|
||||||
|
All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved.
|
||||||
|
A common shell structure for webhook payloads is as follows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "create",
|
||||||
|
"entity": "user",
|
||||||
|
"identifier": "the-user-identifier",
|
||||||
|
"payload": {
|
||||||
|
// The payload of the event, e.g. peer data.
|
||||||
|
// Check the API documentation for the exact structure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example Payload
|
||||||
|
|
||||||
|
The following payload is an example of a webhook event when a peer connects to the VPN:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "connect",
|
||||||
|
"entity": "peer",
|
||||||
|
"identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"payload": {
|
||||||
|
"PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=",
|
||||||
|
"IsConnected": true,
|
||||||
|
"IsPingable": false,
|
||||||
|
"LastPing": null,
|
||||||
|
"BytesReceived": 1860,
|
||||||
|
"BytesTransmitted": 10824,
|
||||||
|
"LastHandshake": "2025-06-26T23:04:33.325216659+02:00",
|
||||||
|
"Endpoint": "10.55.66.77:33874",
|
||||||
|
"LastSessionStart": "2025-06-26T22:50:40.10221606+02:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -24,7 +24,7 @@
|
|||||||
<div id="toasts"></div>
|
<div id="toasts"></div>
|
||||||
|
|
||||||
<!-- main application -->
|
<!-- main application -->
|
||||||
<div id="app"></div>
|
<div id="app" class="d-flex flex-column flex-grow-1"></div>
|
||||||
|
|
||||||
<!-- vue teleport will add modals and dialogs here -->
|
<!-- vue teleport will add modals and dialogs here -->
|
||||||
<div id="modals"></div>
|
<div id="modals"></div>
|
||||||
|
1042
frontend/package-lock.json
generated
1042
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,27 +8,28 @@
|
|||||||
"preview": "vite preview --port 5050"
|
"preview": "vite preview --port 5050"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/nunito-sans": "^5.1.1",
|
"@fontsource/nunito-sans": "^5.2.5",
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@kyvg/vue3-notification": "^3.4.1",
|
"@kyvg/vue3-notification": "^3.4.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.3",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"bootswatch": "^5.3.3",
|
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||||
|
"bootstrap": "^5.3.5",
|
||||||
|
"bootswatch": "^5.3.5",
|
||||||
"flag-icons": "^7.3.2",
|
"flag-icons": "^7.3.2",
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
"is-cidr": "^5.1.0",
|
"is-cidr": "^5.1.1",
|
||||||
"is-ip": "^5.0.1",
|
"is-ip": "^5.0.1",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^3.0.2",
|
||||||
"prismjs": "^1.29.0",
|
"prismjs": "^1.30.0",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.0.1",
|
"vue-i18n": "^11.1.3",
|
||||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0"
|
||||||
"vue3-tags-input": "^1.0.12"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"sass-embedded": "^1.83.4",
|
"sass-embedded": "^1.86.3",
|
||||||
"vite": "^5.4.12"
|
"vite": "6.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|||||||
import { authStore } from "./stores/auth";
|
import { authStore } from "./stores/auth";
|
||||||
import { securityStore } from "./stores/security";
|
import { securityStore } from "./stores/security";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
|
import { Notifications } from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
const appGlobal = getCurrentInstance().appContext.config.globalProperties
|
||||||
const auth = authStore()
|
const auth = authStore()
|
||||||
@ -47,8 +48,11 @@ const languageFlag = computed(() => {
|
|||||||
}
|
}
|
||||||
const langMap = {
|
const langMap = {
|
||||||
en: "us",
|
en: "us",
|
||||||
|
pt: "pt",
|
||||||
uk: "ua",
|
uk: "ua",
|
||||||
zh: "cn",
|
zh: "cn",
|
||||||
|
ko: "kr",
|
||||||
|
|
||||||
};
|
};
|
||||||
return "fi-" + (langMap[lang] || lang);
|
return "fi-" + (langMap[lang] || lang);
|
||||||
})
|
})
|
||||||
@ -57,6 +61,26 @@ const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
|||||||
const wgVersion = ref(WGPORTAL_VERSION);
|
const wgVersion = ref(WGPORTAL_VERSION);
|
||||||
const currentYear = ref(new Date().getFullYear())
|
const currentYear = ref(new Date().getFullYear())
|
||||||
|
|
||||||
|
const userDisplayName = computed(() => {
|
||||||
|
let displayName = "Unknown";
|
||||||
|
if (auth.IsAuthenticated) {
|
||||||
|
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Identifier;
|
||||||
|
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Lastname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Firstname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Firstname + " " + auth.User.Lastname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad string to 20 characters so that the menu is always the same size on desktop
|
||||||
|
if (displayName.length < 20 && window.innerWidth > 992) {
|
||||||
|
displayName = displayName.padStart(20, "\u00A0");
|
||||||
|
}
|
||||||
|
return displayName;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -81,15 +105,19 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
<li v-if="auth.IsAuthenticated && auth.IsAdmin" class="nav-item">
|
||||||
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
<RouterLink :to="{ name: 'users' }" class="nav-link">{{ $t('menu.users') }}</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="navbar-nav d-flex justify-content-end">
|
<div class="navbar-nav d-flex justify-content-end">
|
||||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
||||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
href="#" role="button">{{ userDisplayName }}</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly') || settings.Setting('WebAuthnEnabled')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'audit' }" class="dropdown-item" v-if="auth.IsAdmin"><i class="fas fa-file-shield"></i> {{ $t('menu.audit') }}</RouterLink>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
<a class="dropdown-item" href="#" @click.prevent="auth.Logout"><i class="fas fa-sign-out-alt"></i> {{ $t('menu.logout') }}</a>
|
||||||
</div>
|
</div>
|
||||||
@ -119,16 +147,20 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('de')"><span class="fi fi-de"></span> Deutsch</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('en')"><span class="fi fi-us"></span> English</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('fr')"><span class="fi fi-fr"></span> Français</a>
|
||||||
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ko')"><span class="fi fi-kr"></span> 한국어</a>
|
||||||
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('pt')"><span class="fi fi-pt"></span> Português</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('ru')"><span class="fi fi-ru"></span> Русский</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('uk')"><span class="fi fi-ua"></span> Українська</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('vi')"><span class="fi fi-vi"></span> Tiếng Việt</a>
|
||||||
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
<a class="dropdown-item" href="#" @click.prevent="switchLanguage('zh')"><span class="fi fi-cn"></span> 中文</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer></template>
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -15,3 +15,85 @@ a.disabled {
|
|||||||
.desc::after {
|
.desc::after {
|
||||||
content: " ↓";
|
content: " ↓";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* style the background and the text color of the input ... */
|
||||||
|
.vue-tags-input {
|
||||||
|
max-width: 100% !important;
|
||||||
|
background-color: #f7f7f9 !important;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-input {
|
||||||
|
padding: 0 0;
|
||||||
|
border: none !important;
|
||||||
|
transition: border-bottom 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-new-tag-input {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
padding: 0.75rem 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* style the placeholders color across all browser */
|
||||||
|
.vue-tags-input ::-webkit-input-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
.vue-tags-input .ti-input::placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input ::-moz-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input :-ms-input-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input :-moz-placeholder {
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* default styles for all the tags */
|
||||||
|
.vue-tags-input .ti-tag {
|
||||||
|
position: relative;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2px solid var(--bs-body-color);
|
||||||
|
margin: 6px;
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the styles if a tag is invalid */
|
||||||
|
.vue-tags-input .ti-tag.ti-invalid {
|
||||||
|
background-color: #e88a74;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if the user input is invalid, the input color should be red */
|
||||||
|
.vue-tags-input .ti-new-tag-input.ti-invalid {
|
||||||
|
color: #e88a74;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if a tag or the user input is a duplicate, it should be crossed out */
|
||||||
|
.vue-tags-input .ti-duplicate span,
|
||||||
|
.vue-tags-input .ti-new-tag-input.ti-duplicate {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if the user presses backspace, the complete tag should be crossed out, to mark it for deletion */
|
||||||
|
.vue-tags-input .ti-tag:after {
|
||||||
|
transition: transform .2s;
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
height: 2px;
|
||||||
|
width: 108%;
|
||||||
|
left: -4%;
|
||||||
|
top: calc(50% - 1px);
|
||||||
|
background-color: #000;
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-tags-input .ti-deletion-mark:after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
@ -5,6 +5,10 @@ $web-font-path: false;
|
|||||||
@import "bootstrap/scss/bootstrap";
|
@import "bootstrap/scss/bootstrap";
|
||||||
@import "bootswatch/dist/lux/bootswatch";
|
@import "bootswatch/dist/lux/bootswatch";
|
||||||
|
|
||||||
|
// fix strange border width bug in bootswatch 5.3
|
||||||
|
:root {
|
||||||
|
--bs-border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
// for future use, once bootswatch supports @use
|
// for future use, once bootswatch supports @use
|
||||||
/*
|
/*
|
||||||
|
@ -4,7 +4,7 @@ import {interfaceStore} from "@/stores/interfaces";
|
|||||||
import {computed, ref, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from 'vue3-tags-input';
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
@ -38,6 +38,15 @@ const title = computed(() => {
|
|||||||
return t("modals.interface-edit.headline-new")
|
return t("modals.interface-edit.headline-new")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentTags = ref({
|
||||||
|
Addresses: "",
|
||||||
|
Dns: "",
|
||||||
|
DnsSearch: "",
|
||||||
|
PeerDefNetwork: "",
|
||||||
|
PeerDefAllowedIPs: "",
|
||||||
|
PeerDefDns: "",
|
||||||
|
PeerDefDnsSearch: ""
|
||||||
|
})
|
||||||
const formData = ref(freshInterface())
|
const formData = ref(freshInterface())
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
@ -137,94 +146,94 @@ function close() {
|
|||||||
function handleChangeAddresses(tags) {
|
function handleChangeAddresses(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.Addresses = tags
|
formData.value.Addresses = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDns(tags) {
|
function handleChangeDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(!isIP(tag)) {
|
if(!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.Dns = tags
|
formData.value.Dns = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDnsSearch(tags) {
|
function handleChangeDnsSearch(tags) {
|
||||||
formData.value.DnsSearch = tags
|
formData.value.DnsSearch = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefNetwork(tags) {
|
function handleChangePeerDefNetwork(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefNetwork = tags
|
formData.value.PeerDefNetwork = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefAllowedIPs(tags) {
|
function handleChangePeerDefAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(isCidr(tag) === 0) {
|
if(isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefAllowedIPs = tags
|
formData.value.PeerDefAllowedIPs = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefDns(tags) {
|
function handleChangePeerDefDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if(!isIP(tag)) {
|
if(!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.PeerDefDns = tags
|
formData.value.PeerDefDns = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangePeerDefDnsSearch(tags) {
|
function handleChangePeerDefDnsSearch(tags) {
|
||||||
formData.value.PeerDefDnsSearch = tags
|
formData.value.PeerDefDnsSearch = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@ -322,22 +331,26 @@ async function del() {
|
|||||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
<legend class="mt-4">{{ $t('modals.interface-edit.header-crypto') }}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.private-key.label') }}</label>
|
||||||
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="email">
|
<input v-model="formData.PrivateKey" class="form-control" :placeholder="$t('modals.interface-edit.private-key.placeholder')" required type="text">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.public-key.label') }}</label>
|
||||||
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="email">
|
<input v-model="formData.PublicKey" class="form-control" :placeholder="$t('modals.interface-edit.public-key.placeholder')" required type="text">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
<legend class="mt-4">{{ $t('modals.interface-edit.header-network') }}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||||
|
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
:placeholder="$t('modals.interface-edit.ip.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateCIDR()"
|
||||||
:validate="validateCIDR"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangeAddresses"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAddresses"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode==='server'" class="form-group">
|
<div v-if="formData.Mode==='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.listen-port.label') }}</label>
|
||||||
@ -345,19 +358,27 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Dns"
|
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||||
|
:tags="formData.Dns.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateIP()"
|
||||||
:validate="validateIP"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangeDns"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDns"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Mode!=='server'" class="form-group">
|
<div v-if="formData.Mode!=='server'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch"
|
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||||
|
:tags="formData.DnsSearch.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateDomain()"
|
||||||
:validate="validateDomain"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangeDnsSearch"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDnsSearch"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
@ -420,36 +441,52 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.networks.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefNetwork"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefNetwork"
|
||||||
|
:tags="formData.PeerDefNetwork.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
:placeholder="$t('modals.interface-edit.defaults.networks.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateCIDR()"
|
||||||
:validate="validateCIDR"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangePeerDefNetwork"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefNetwork"/>
|
||||||
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.interface-edit.defaults.networks.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.defaults.allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefAllowedIPs"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefAllowedIPs"
|
||||||
|
:tags="formData.PeerDefAllowedIPs.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
:placeholder="$t('modals.interface-edit.defaults.allowed-ip.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateCIDR()"
|
||||||
:validate="validateCIDR"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangePeerDefAllowedIPs"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefAllowedIPs"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDns"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDns"
|
||||||
|
:tags="formData.PeerDefDns.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
:placeholder="$t('modals.interface-edit.dns.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateIP()"
|
||||||
:validate="validateIP"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangePeerDefDns"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefDns"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.interface-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.PeerDefDnsSearch"
|
<vue-tags-input class="form-control" v-model="currentTags.PeerDefDnsSearch"
|
||||||
|
:tags="formData.PeerDefDnsSearch.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
:placeholder="$t('modals.interface-edit.dns-search.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:validation="validateDomain()"
|
||||||
:validate="validateDomain"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangePeerDefDnsSearch"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangePeerDefDnsSearch"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
|
@ -5,7 +5,7 @@ import { interfaceStore } from "@/stores/interfaces";
|
|||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from "vue3-tags-input";
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
|
||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import { isIP } from 'is-ip';
|
import { isIP } from 'is-ip';
|
||||||
@ -65,6 +65,13 @@ const title = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentTags = ref({
|
||||||
|
Addresses: "",
|
||||||
|
AllowedIPs: "",
|
||||||
|
ExtraAllowedIPs: "",
|
||||||
|
Dns: "",
|
||||||
|
DnsSearch: ""
|
||||||
|
})
|
||||||
const formData = ref(freshPeer())
|
const formData = ref(freshPeer())
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
@ -193,73 +200,73 @@ function close() {
|
|||||||
function handleChangeAddresses(tags) {
|
function handleChangeAddresses(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.Addresses = tags
|
formData.value.Addresses = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeAllowedIPs(tags) {
|
function handleChangeAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.AllowedIPs.Value = tags
|
formData.value.AllowedIPs.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeExtraAllowedIPs(tags) {
|
function handleChangeExtraAllowedIPs(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (isCidr(tag) === 0) {
|
if (isCidr(tag.text) === 0) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid CIDR",
|
title: "Invalid CIDR",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.ExtraAllowedIPs = tags
|
formData.value.ExtraAllowedIPs = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDns(tags) {
|
function handleChangeDns(tags) {
|
||||||
let validInput = true
|
let validInput = true
|
||||||
tags.forEach(tag => {
|
tags.forEach(tag => {
|
||||||
if (!isIP(tag)) {
|
if (!isIP(tag.text)) {
|
||||||
validInput = false
|
validInput = false
|
||||||
notify({
|
notify({
|
||||||
title: "Invalid IP",
|
title: "Invalid IP",
|
||||||
text: tag + " is not a valid IP address",
|
text: tag.text + " is not a valid IP address",
|
||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (validInput) {
|
if (validInput) {
|
||||||
formData.value.Dns.Value = tags
|
formData.value.Dns.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeDnsSearch(tags) {
|
function handleChangeDnsSearch(tags) {
|
||||||
formData.value.DnsSearch.Value = tags
|
formData.value.DnsSearch.Value = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@ -316,17 +323,18 @@ async function del() {
|
|||||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||||
<div class="form-group" v-if="selectedInterface.Mode === 'server'">
|
<div class="form-group" v-if="selectedInterface.Mode === 'server'">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||||
v-model="formData.PrivateKey">
|
v-model="formData.PrivateKey">
|
||||||
|
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||||
v-model="formData.PublicKey">
|
v-model="formData.PublicKey">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||||
v-model="formData.PresharedKey">
|
v-model="formData.PresharedKey">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" v-if="formData.Mode === 'client'">
|
<div class="form-group" v-if="formData.Mode === 'client'">
|
||||||
@ -344,34 +352,64 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Addresses"
|
<vue-tags-input class="form-control" v-model="currentTags.Addresses"
|
||||||
:placeholder="$t('modals.peer-edit.ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.Addresses.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeAddresses" />
|
:placeholder="$t('modals.peer-edit.ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAddresses" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.AllowedIPs"
|
||||||
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.AllowedIPs.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeAllowedIPs" />
|
:placeholder="$t('modals.peer-edit.allowed-ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeAllowedIPs" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.extra-allowed-ip.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
|
<vue-tags-input class="form-control" v-model="currentTags.ExtraAllowedIPs"
|
||||||
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.ExtraAllowedIPs.map(str => ({ text: str }))"
|
||||||
:validate="validateCIDR" @on-tags-changed="handleChangeExtraAllowedIPs" />
|
:placeholder="$t('modals.peer-edit.extra-allowed-ip.placeholder')"
|
||||||
|
:validation="validateCIDR()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeExtraAllowedIPs" />
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-edit.extra-allowed-ip.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Dns.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.Dns"
|
||||||
:placeholder="$t('modals.peer-edit.dns.placeholder')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.Dns.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateIP" @on-tags-changed="handleChangeDns" />
|
:placeholder="$t('modals.peer-edit.dns.placeholder')"
|
||||||
|
:validation="validateIP()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDns" />
|
||||||
</div>
|
</div>
|
||||||
<div hidden class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.dns-search.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
|
<vue-tags-input class="form-control" v-model="currentTags.DnsSearch"
|
||||||
:placeholder="$t('modals.peer-edit.dns-search.label')" :add-tag-on-keys="[13, 188, 32, 9]"
|
:tags="formData.DnsSearch.Value.map(str => ({ text: str }))"
|
||||||
:validate="validateDomain" @on-tags-changed="handleChangeDnsSearch" />
|
:placeholder="$t('modals.peer-edit.dns-search.label')"
|
||||||
|
:validation="validateDomain()"
|
||||||
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeDnsSearch" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
|
@ -5,7 +5,7 @@ import {interfaceStore} from "@/stores/interfaces";
|
|||||||
import {computed, ref} from "vue";
|
import {computed, ref} from "vue";
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import Vue3TagsInput from "vue3-tags-input";
|
import { VueTagsInput } from '@vojtechlanka/vue-tags-input';
|
||||||
import { freshInterface } from '@/helpers/models';
|
import { freshInterface } from '@/helpers/models';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -36,6 +36,7 @@ function freshForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentTag = ref("")
|
||||||
const formData = ref(freshForm())
|
const formData = ref(freshForm())
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
@ -55,7 +56,7 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeUserIdentifiers(tags) {
|
function handleChangeUserIdentifiers(tags) {
|
||||||
formData.value.Identifiers = tags
|
formData.value.Identifiers = tags.map(tag => tag.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
@ -89,10 +90,14 @@ async function save() {
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.identifiers.label') }}</label>
|
||||||
<vue3-tags-input class="form-control" :tags="formData.Identifiers"
|
<vue-tags-input class="form-control" v-model="currentTag"
|
||||||
|
:tags="formData.Identifiers.map(str => ({ text: str }))"
|
||||||
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
:placeholder="$t('modals.peer-multi-create.identifiers.placeholder')"
|
||||||
:add-tag-on-keys="[13, 188, 32, 9]"
|
:add-on-key="[13, 188, 32, 9]"
|
||||||
@on-tags-changed="handleChangeUserIdentifiers"/>
|
:save-on-key="[13, 188, 32, 9]"
|
||||||
|
:allow-edit-tags="true"
|
||||||
|
:separators="[',', ';', ' ']"
|
||||||
|
@tags-changed="handleChangeUserIdentifiers"/>
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.identifiers.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -89,19 +89,11 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
|
|
||||||
function download() {
|
function download() {
|
||||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||||
let filename = 'WireGuard-Tunnel.conf'
|
|
||||||
if (selectedPeer.value.DisplayName) {
|
|
||||||
filename = selectedPeer.value.DisplayName
|
|
||||||
.replace(/ /g, "_")
|
|
||||||
.replace(/[^a-zA-Z0-9-_]/g, "")
|
|
||||||
.substring(0, 16)
|
|
||||||
+ ".conf"
|
|
||||||
}
|
|
||||||
let text = configString.value
|
let text = configString.value
|
||||||
|
|
||||||
let element = document.createElement('a')
|
let element = document.createElement('a')
|
||||||
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
||||||
element.setAttribute('download', filename)
|
element.setAttribute('download', selectedPeer.value.Filename)
|
||||||
|
|
||||||
element.style.display = 'none'
|
element.style.display = 'none'
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element)
|
||||||
|
@ -5,10 +5,12 @@ import {computed, ref, watch} from "vue";
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import {freshUser} from "@/helpers/models";
|
import {freshUser} from "@/helpers/models";
|
||||||
|
import {settingsStore} from "@/stores/settings";
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const users = userStore()
|
const users = userStore()
|
||||||
|
const settings = settingsStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
userId: String,
|
userId: String,
|
||||||
@ -33,6 +35,30 @@ const title = computed(() => {
|
|||||||
|
|
||||||
const formData = ref(freshUser())
|
const formData = ref(freshUser())
|
||||||
|
|
||||||
|
const passwordWeak = computed(() => {
|
||||||
|
return formData.value.Password && formData.value.Password.length > 0 && formData.value.Password.length < settings.Setting('MinPasswordLength')
|
||||||
|
})
|
||||||
|
|
||||||
|
const formValid = computed(() => {
|
||||||
|
if (formData.value.Source !== 'db') {
|
||||||
|
return true // nothing to validate
|
||||||
|
}
|
||||||
|
if (props.userId !== '#NEW#' && passwordWeak.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (props.userId === '#NEW#' && (!formData.value.Password || formData.value.Password.length < 1)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (props.userId === '#NEW#' && passwordWeak.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!formData.value.Identifier || formData.value.Identifier.length < 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
|
|
||||||
watch(() => props.visible, async (newValue, oldValue) => {
|
watch(() => props.visible, async (newValue, oldValue) => {
|
||||||
@ -51,6 +77,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
|||||||
formData.value.Notes = selectedUser.value.Notes
|
formData.value.Notes = selectedUser.value.Notes
|
||||||
formData.value.Password = ""
|
formData.value.Password = ""
|
||||||
formData.value.Disabled = selectedUser.value.Disabled
|
formData.value.Disabled = selectedUser.value.Disabled
|
||||||
|
formData.value.Locked = selectedUser.value.Locked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +135,8 @@ async function del() {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="formData.Source==='db'" class="form-group">
|
<div v-if="formData.Source==='db'" class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :placeholder="$t('modals.user-edit.password.placeholder')" type="text">
|
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :class="{ 'is-invalid': passwordWeak, 'is-valid': formData.Password !== '' && !passwordWeak }" :placeholder="$t('modals.user-edit.password.placeholder')" type="password">
|
||||||
|
<div class="invalid-feedback">{{ $t('modals.user-edit.password.too-weak') }}</div>
|
||||||
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
<small v-if="props.userId!=='#NEW#'" id="passwordHelp" class="form-text text-muted">{{ $t('modals.user-edit.password.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -167,7 +195,7 @@ async function del() {
|
|||||||
<div class="flex-fill text-start">
|
<div class="flex-fill text-start">
|
||||||
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
<button v-if="props.userId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">{{ $t('general.delete') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary me-1" type="button" @click.prevent="save">{{ $t('general.save') }}</button>
|
<button class="btn btn-primary me-1" type="button" @click.prevent="save" :disabled="!formValid">{{ $t('general.save') }}</button>
|
||||||
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
<button class="btn btn-secondary" type="button" @click.prevent="close">{{ $t('general.close') }}</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -211,17 +211,18 @@ async function del() {
|
|||||||
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
<legend class="mt-4">{{ $t('modals.peer-edit.header-crypto') }}</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.private-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.private-key.placeholder')" required
|
||||||
v-model="formData.PrivateKey">
|
v-model="formData.PrivateKey">
|
||||||
|
<small id="privateKeyHelp" class="form-text text-muted">{{ $t('modals.peer-edit.private-key.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.public-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.public-key.placeholder')" required
|
||||||
v-model="formData.PublicKey">
|
v-model="formData.PublicKey">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-edit.preshared-key.label') }}</label>
|
||||||
<input type="email" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-edit.preshared-key.placeholder')"
|
||||||
v-model="formData.PresharedKey">
|
v-model="formData.PresharedKey">
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@ -42,7 +42,8 @@ export function freshInterface() {
|
|||||||
PeerDefPostDown: "",
|
PeerDefPostDown: "",
|
||||||
|
|
||||||
TotalPeers: 0,
|
TotalPeers: 0,
|
||||||
EnabledPeers: 0
|
EnabledPeers: 0,
|
||||||
|
Filename: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +121,11 @@ export function freshPeer() {
|
|||||||
Overridable: true,
|
Overridable: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Internal value
|
Filename: "",
|
||||||
IgnoreGlobalSettings: false
|
|
||||||
|
// Internal values
|
||||||
|
IgnoreGlobalSettings: false,
|
||||||
|
IsSelected: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +152,10 @@ export function freshUser() {
|
|||||||
|
|
||||||
ApiEnabled: false,
|
ApiEnabled: false,
|
||||||
|
|
||||||
PeerCount: 0
|
PeerCount: 0,
|
||||||
|
|
||||||
|
// Internal values
|
||||||
|
IsSelected: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import isCidr from "is-cidr";
|
import isCidr from "is-cidr";
|
||||||
import {isIP} from 'is-ip';
|
import {isIP} from 'is-ip';
|
||||||
|
|
||||||
export function validateCIDR(value) {
|
export function validateCIDR() {
|
||||||
return isCidr(value) !== 0
|
return [{
|
||||||
|
classes: 'invalid-cidr',
|
||||||
|
rule: ({ text }) => isCidr(text) === 0,
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateIP(value) {
|
export function validateIP() {
|
||||||
return isIP(value)
|
return [{
|
||||||
|
classes: 'invalid-ip',
|
||||||
|
rule: ({ text }) => !isIP(text),
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateDomain(value) {
|
export function validateDomain() {
|
||||||
return true
|
return [{
|
||||||
|
classes: 'invalid-domain',
|
||||||
|
rule: tag => tag.text.length < 3,
|
||||||
|
disableAdd: true,
|
||||||
|
}]
|
||||||
}
|
}
|
@ -2,10 +2,13 @@
|
|||||||
import de from './translations/de.json';
|
import de from './translations/de.json';
|
||||||
import en from './translations/en.json';
|
import en from './translations/en.json';
|
||||||
import fr from './translations/fr.json';
|
import fr from './translations/fr.json';
|
||||||
|
import ko from './translations/ko.json';
|
||||||
|
import pt from './translations/pt.json';
|
||||||
import ru from './translations/ru.json';
|
import ru from './translations/ru.json';
|
||||||
import uk from './translations/uk.json';
|
import uk from './translations/uk.json';
|
||||||
import vi from './translations/vi.json';
|
import vi from './translations/vi.json';
|
||||||
import zh from './translations/zh.json';
|
import zh from './translations/zh.json';
|
||||||
|
|
||||||
import {createI18n} from "vue-i18n";
|
import {createI18n} from "vue-i18n";
|
||||||
|
|
||||||
// Create i18n instance with options
|
// Create i18n instance with options
|
||||||
@ -23,6 +26,8 @@ const i18n = createI18n({
|
|||||||
"de": de,
|
"de": de,
|
||||||
"en": en,
|
"en": en,
|
||||||
"fr": fr,
|
"fr": fr,
|
||||||
|
"ko": ko,
|
||||||
|
"pt": pt,
|
||||||
"ru": ru,
|
"ru": ru,
|
||||||
"uk": uk,
|
"uk": uk,
|
||||||
"vi": vi,
|
"vi": vi,
|
||||||
|
@ -26,10 +26,11 @@
|
|||||||
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
|
"placeholder": "Bitte geben Sie Ihren Benutzernamen ein"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Kennwort",
|
"label": "Passwort",
|
||||||
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
"placeholder": "Bitte geben Sie Ihr Passwort ein"
|
||||||
},
|
},
|
||||||
"button": "Anmelden"
|
"button": "Anmelden",
|
||||||
|
"button-webauthn": "Passkey verwenden"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@ -38,8 +39,10 @@
|
|||||||
"lang": "Sprache ändern",
|
"lang": "Sprache ändern",
|
||||||
"profile": "Mein Profil",
|
"profile": "Mein Profil",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
|
"audit": "Event Protokoll",
|
||||||
"login": "Anmelden",
|
"login": "Anmelden",
|
||||||
"logout": "Abmelden"
|
"logout": "Abmelden",
|
||||||
|
"keygen": "Schlüsselgenerator"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"headline": "WireGuard® VPN Portal",
|
"headline": "WireGuard® VPN Portal",
|
||||||
@ -79,77 +82,77 @@
|
|||||||
},
|
},
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"headline": "Schnittstellenverwaltung",
|
"headline": "Schnittstellenverwaltung",
|
||||||
"headline-peers": "Current VPN Peers",
|
"headline-peers": "Aktuelle VPN-Peers",
|
||||||
"headline-endpoints": "Current Endpoints",
|
"headline-endpoints": "Aktuelle Endpunkte",
|
||||||
"no-interface": {
|
"no-interface": {
|
||||||
"default-selection": "No Interface available",
|
"default-selection": "Keine Schnittstelle verfügbar",
|
||||||
"headline": "No interfaces found...",
|
"headline": "Keine Schnittstellen gefunden...",
|
||||||
"abstract": "Click the plus button above to create a new WireGuard interface."
|
"abstract": "Klicken Sie auf die Plus-Schaltfläche oben, um eine neue WireGuard-Schnittstelle zu erstellen."
|
||||||
},
|
},
|
||||||
"no-peer": {
|
"no-peer": {
|
||||||
"headline": "No peers available",
|
"headline": "Keine Peers verfügbar",
|
||||||
"abstract": "Currently, there are no peers available for the selected WireGuard interface."
|
"abstract": "Derzeit sind keine Peers für die ausgewählte WireGuard-Schnittstelle verfügbar."
|
||||||
},
|
},
|
||||||
"table-heading": {
|
"table-heading": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"user": "User",
|
"user": "Benutzer",
|
||||||
"ip": "IP's",
|
"ip": "IP's",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpunkt",
|
||||||
"status": "Status"
|
"status": "Status"
|
||||||
},
|
},
|
||||||
"interface": {
|
"interface": {
|
||||||
"headline": "Interface status for",
|
"headline": "Schnittstellenstatus für",
|
||||||
"mode": "mode",
|
"mode": "Modus",
|
||||||
"key": "Public Key",
|
"key": "Öffentlicher Schlüssel",
|
||||||
"endpoint": "Public Endpoint",
|
"endpoint": "Öffentlicher Endpunkt",
|
||||||
"port": "Listening Port",
|
"port": "Port",
|
||||||
"peers": "Enabled Peers",
|
"peers": "Aktive Peers",
|
||||||
"total-peers": "Total Peers",
|
"total-peers": "Gesamtanzahl Peers",
|
||||||
"endpoints": "Enabled Endpoints",
|
"endpoints": "Aktive Endpunkte",
|
||||||
"total-endpoints": "Total Endpoints",
|
"total-endpoints": "Gesamtanzahl Endpunkte",
|
||||||
"ip": "IP Address",
|
"ip": "IP-Adresse",
|
||||||
"default-allowed-ip": "Default allowed IPs",
|
"default-allowed-ip": "Standard Erlaubte-IPs",
|
||||||
"dns": "DNS Servers",
|
"dns": "DNS-Server",
|
||||||
"mtu": "MTU",
|
"mtu": "MTU",
|
||||||
"default-keep-alive": "Default Keepalive Interval",
|
"default-keep-alive": "Standard Keepalive-Intervall",
|
||||||
"button-show-config": "Show configuration",
|
"button-show-config": "Konfiguration anzeigen",
|
||||||
"button-download-config": "Download configuration",
|
"button-download-config": "Konfiguration herunterladen",
|
||||||
"button-store-config": "Store configuration for wg-quick",
|
"button-store-config": "Konfiguration für wg-quick speichern",
|
||||||
"button-edit": "Edit interface"
|
"button-edit": "Schnittstelle bearbeiten"
|
||||||
},
|
},
|
||||||
"button-add-interface": "Add Interface",
|
"button-add-interface": "Schnittstelle hinzufügen",
|
||||||
"button-add-peer": "Add Peer",
|
"button-add-peer": "Peer hinzufügen",
|
||||||
"button-add-peers": "Add Multiple Peers",
|
"button-add-peers": "Mehrere Peers hinzufügen",
|
||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Peer anzeigen",
|
||||||
"button-edit-peer": "Edit Peer",
|
"button-edit-peer": "Peer bearbeiten",
|
||||||
"peer-disabled": "Peer is disabled, reason:",
|
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
||||||
"peer-expiring": "Peer is expiring at",
|
"peer-expiring": "Peer läuft ab am",
|
||||||
"peer-connected": "Connected",
|
"peer-connected": "Verbunden",
|
||||||
"peer-not-connected": "Not Connected",
|
"peer-not-connected": "Nicht verbunden",
|
||||||
"peer-handshake": "Last handshake:"
|
"peer-handshake": "Letzter Handshake:"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"headline": "Benutzerverwaltung",
|
"headline": "Benutzerverwaltung",
|
||||||
"table-heading": {
|
"table-heading": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"firstname": "Firstname",
|
"firstname": "Vorname",
|
||||||
"lastname": "Lastname",
|
"lastname": "Nachname",
|
||||||
"source": "Source",
|
"source": "Quelle",
|
||||||
"peers": "Peers",
|
"peers": "Peers",
|
||||||
"admin": "Admin"
|
"admin": "Admin"
|
||||||
},
|
},
|
||||||
"no-user": {
|
"no-user": {
|
||||||
"headline": "No users available",
|
"headline": "Keine Benutzer verfügbar",
|
||||||
"abstract": "Currently, there are no users registered with WireGuard Portal."
|
"abstract": "Derzeit sind keine Benutzer im WireGuard-Portal registriert."
|
||||||
},
|
},
|
||||||
"button-add-user": "Add User",
|
"button-add-user": "Benutzer hinzufügen",
|
||||||
"button-show-user": "Show User",
|
"button-show-user": "Benutzer anzeigen",
|
||||||
"button-edit-user": "Edit User",
|
"button-edit-user": "Benutzer bearbeiten",
|
||||||
"user-disabled": "User is disabled, reason:",
|
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
||||||
"user-locked": "Account is locked, reason:",
|
"user-locked": "Konto ist gesperrt, Grund:",
|
||||||
"admin": "User has administrator privileges",
|
"admin": "Benutzer hat Administratorrechte",
|
||||||
"no-admin": "User has no administrator privileges"
|
"no-admin": "Benutzer hat keine Administratorrechte"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"headline": "Meine VPN-Konfigurationen",
|
"headline": "Meine VPN-Konfigurationen",
|
||||||
@ -157,16 +160,16 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"ip": "IP's",
|
"ip": "IP's",
|
||||||
"stats": "Status",
|
"stats": "Status",
|
||||||
"interface": "Server Interface"
|
"interface": "Server-Schnittstelle"
|
||||||
},
|
},
|
||||||
"no-peer": {
|
"no-peer": {
|
||||||
"headline": "No peers available",
|
"headline": "Keine Peers verfügbar",
|
||||||
"abstract": "Currently, there are no peers associated with your user profile."
|
"abstract": "Derzeit sind keine Peers mit Ihrem Benutzerprofil verknüpft."
|
||||||
},
|
},
|
||||||
"peer-connected": "Connected",
|
"peer-connected": "Verbunden",
|
||||||
"button-add-peer": "Add Peer",
|
"button-add-peer": "Peer hinzufügen",
|
||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Peer anzeigen",
|
||||||
"button-edit-peer": "Edit Peer"
|
"button-edit-peer": "Peer bearbeiten"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"headline": "Einstellungen",
|
"headline": "Einstellungen",
|
||||||
@ -186,327 +189,394 @@
|
|||||||
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
"button-enable-title": "Aktivieren Sie die API, dadurch wird ein neuer Token generiert.",
|
||||||
"button-enable-text": "API aktivieren",
|
"button-enable-text": "API aktivieren",
|
||||||
"api-link": "API Dokumentation"
|
"api-link": "API Dokumentation"
|
||||||
|
},
|
||||||
|
"webauthn": {
|
||||||
|
"headline": "Passkey-Einstellungen",
|
||||||
|
"abstract": "Passkeys sind eine moderne Möglichkeit, Benutzer ohne Passwort zu authentifizieren. Sie werden sicher in Ihrem Browser gespeichert und können verwendet werden, um sich im WireGuard-Portal anzumelden.",
|
||||||
|
"active-description": "Mindestens ein Passkey ist derzeit für Ihr Benutzerkonto aktiv.",
|
||||||
|
"inactive-description": "Für Ihr Benutzerkonto sind derzeit keine Passkeys registriert. Drücken Sie die Schaltfläche unten, um einen neuen Passkey zu registrieren.",
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"created": "Erstellt",
|
||||||
|
"actions": ""
|
||||||
|
},
|
||||||
|
"credentials-list": "Derzeit registrierte Passkeys",
|
||||||
|
"modal-delete": {
|
||||||
|
"headline": "Passkey löschen",
|
||||||
|
"abstract": "Sind Sie sicher, dass Sie diesen Passkey löschen möchten? Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||||
|
"created": "Erstellt:",
|
||||||
|
"button-delete": "Löschen",
|
||||||
|
"button-cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
"button-rename-title": "Umbenennen",
|
||||||
|
"button-rename-text": "Passkey umbenennen.",
|
||||||
|
"button-save-title": "Speichern",
|
||||||
|
"button-save-text": "Neuen Namen des Passkeys speichern.",
|
||||||
|
"button-cancel-title": "Abbrechen",
|
||||||
|
"button-cancel-text": "Umbenennung des Passkeys abbrechen.",
|
||||||
|
"button-delete-title": "Löschen",
|
||||||
|
"button-delete-text": "Passkey löschen. Sie können sich anschließend nicht mehr mit diesem Passkey anmelden.",
|
||||||
|
"button-register-title": "Passkey registrieren",
|
||||||
|
"button-register-text": "Einen neuen Passkey registrieren, um Ihr Konto zu sichern."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "Eventprotokoll",
|
||||||
|
"abstract": "Hier finden Sie das Eventprotokoll aller im WireGuard-Portal vorgenommenen Aktionen.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "Keine Protokolleinträge verfügbar",
|
||||||
|
"abstract": "Derzeit sind keine Eventprotokolle aufgezeichnet."
|
||||||
|
},
|
||||||
|
"entries-headline": "Protokolleinträge",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "Zeit",
|
||||||
|
"user": "Benutzer",
|
||||||
|
"severity": "Schweregrad",
|
||||||
|
"origin": "Ursprung",
|
||||||
|
"message": "Nachricht"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keygen": {
|
||||||
|
"headline": "WireGuard Key Generator",
|
||||||
|
"abstract": "Hier können Sie WireGuard Schlüsselpaare generieren. Die Schlüssel werden lokal auf Ihrem Computer generiert und niemals an den Server gesendet.",
|
||||||
|
"headline-keypair": "Neues Schlüsselpaar",
|
||||||
|
"headline-preshared-key": "Neuer Pre-Shared Key",
|
||||||
|
"button-generate": "Erzeugen",
|
||||||
|
"private-key": {
|
||||||
|
"label": "Privater Schlüssel",
|
||||||
|
"placeholder": "Der private Schlüssel"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Öffentlicher Schlüssel",
|
||||||
|
"placeholder": "Der öffentliche Schlüssel"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "Pre-Shared Key",
|
||||||
|
"placeholder": "Der geteilte Schlüssel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
"user-view": {
|
"user-view": {
|
||||||
"headline": "User Account:",
|
"headline": "Benutzerkonto:",
|
||||||
"tab-user": "Information",
|
"tab-user": "Informationen",
|
||||||
"tab-peers": "Peers",
|
"tab-peers": "Peers",
|
||||||
"headline-info": "User Information:",
|
"headline-info": "Benutzerinformationen:",
|
||||||
"headline-notes": "Notes:",
|
"headline-notes": "Notizen:",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"firstname": "Firstname",
|
"firstname": "Vorname",
|
||||||
"lastname": "Lastname",
|
"lastname": "Nachname",
|
||||||
"phone": "Phone number",
|
"phone": "Telefonnummer",
|
||||||
"department": "Department",
|
"department": "Abteilung",
|
||||||
"disabled": "Account Disabled",
|
"api-enabled": "API-Zugriff",
|
||||||
"locked": "Account Locked",
|
"disabled": "Konto deaktiviert",
|
||||||
"no-peers": "User has no associated peers.",
|
"locked": "Konto gesperrt",
|
||||||
|
"no-peers": "Benutzer hat keine zugeordneten Peers.",
|
||||||
"peers": {
|
"peers": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"interface": "Interface",
|
"interface": "Schnittstelle",
|
||||||
"ip": "IP's"
|
"ip": "IP's"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user-edit": {
|
"user-edit": {
|
||||||
"headline-edit": "Edit user:",
|
"headline-edit": "Benutzer bearbeiten:",
|
||||||
"headline-new": "New user",
|
"headline-new": "Neuer Benutzer",
|
||||||
"header-general": "General",
|
"header-general": "Allgemein",
|
||||||
"header-personal": "User Information",
|
"header-personal": "Benutzerinformationen",
|
||||||
"header-notes": "Notes",
|
"header-notes": "Notizen",
|
||||||
"header-state": "State",
|
"header-state": "Status",
|
||||||
"identifier": {
|
"identifier": {
|
||||||
"label": "Identifier",
|
"label": "Kennung",
|
||||||
"placeholder": "The unique user identifier"
|
"placeholder": "Die eindeutige Benutzerkennung"
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"label": "Source",
|
"label": "Quelle",
|
||||||
"placeholder": "The user source"
|
"placeholder": "Die Benutzerquelle"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Passwort",
|
||||||
"placeholder": "A super secret password",
|
"placeholder": "Ein super geheimes Passwort",
|
||||||
"description": "Leave this field blank to keep current password."
|
"description": "Lassen Sie dieses Feld leer, um das aktuelle Passwort beizubehalten.",
|
||||||
|
"too-weak": "Das Passwort entspricht nicht den Sicherheitsanforderungen."
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"label": "Email",
|
"label": "E-Mail",
|
||||||
"placeholder": "The email address"
|
"placeholder": "Die E-Mail-Adresse"
|
||||||
},
|
},
|
||||||
"phone": {
|
"phone": {
|
||||||
"label": "Phone",
|
"label": "Telefon",
|
||||||
"placeholder": "The phone number"
|
"placeholder": "Die Telefonnummer"
|
||||||
},
|
},
|
||||||
"department": {
|
"department": {
|
||||||
"label": "Department",
|
"label": "Abteilung",
|
||||||
"placeholder": "The department"
|
"placeholder": "Die Abteilung"
|
||||||
},
|
},
|
||||||
"firstname": {
|
"firstname": {
|
||||||
"label": "Firstname",
|
"label": "Vorname",
|
||||||
"placeholder": "Firstname"
|
"placeholder": "Vorname"
|
||||||
},
|
},
|
||||||
"lastname": {
|
"lastname": {
|
||||||
"label": "Lastname",
|
"label": "Nachname",
|
||||||
"placeholder": "Lastname"
|
"placeholder": "Nachname"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"label": "Notes",
|
"label": "Notizen",
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
},
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"label": "Disabled (no WireGuard connection and no login possible)"
|
"label": "Deaktiviert (keine WireGuard-Verbindung und kein Login möglich)"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"label": "Locked (no login possible, WireGuard connections still work)"
|
"label": "Gesperrt (kein Login möglich, WireGuard-Verbindungen funktionieren weiterhin)"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"label": "Is Admin"
|
"label": "Ist Administrator"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"interface-view": {
|
"interface-view": {
|
||||||
"headline": "Config for Interface:"
|
"headline": "Konfiguration für Schnittstelle:"
|
||||||
},
|
},
|
||||||
"interface-edit": {
|
"interface-edit": {
|
||||||
"headline-edit": "Edit Interface:",
|
"headline-edit": "Schnittstelle bearbeiten:",
|
||||||
"headline-new": "New Interface",
|
"headline-new": "Neue Schnittstelle",
|
||||||
"tab-interface": "Interface",
|
"tab-interface": "Schnittstelle",
|
||||||
"tab-peerdef": "Peer Defaults",
|
"tab-peerdef": "Peer-Standardeinstellungen",
|
||||||
"header-general": "General",
|
"header-general": "Allgemein",
|
||||||
"header-network": "Network",
|
"header-network": "Netzwerk",
|
||||||
"header-crypto": "Cryptography",
|
"header-crypto": "Kryptografie",
|
||||||
"header-hooks": "Interface Hooks",
|
"header-hooks": "Schnittstellen-Hooks",
|
||||||
"header-peer-hooks": "Hooks",
|
"header-peer-hooks": "Hooks",
|
||||||
"header-state": "State",
|
"header-state": "Status",
|
||||||
"identifier": {
|
"identifier": {
|
||||||
"label": "Identifier",
|
"label": "Kennung",
|
||||||
"placeholder": "The unique interface identifier"
|
"placeholder": "Die eindeutige Schnittstellenkennung"
|
||||||
},
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"label": "Interface Mode",
|
"label": "Schnittstellenmodus",
|
||||||
"server": "Server Mode",
|
"server": "Server-Modus",
|
||||||
"client": "Client Mode",
|
"client": "Client-Modus",
|
||||||
"any": "Unknown Mode"
|
"any": "Unbekannter Modus"
|
||||||
},
|
},
|
||||||
"display-name": {
|
"display-name": {
|
||||||
"label": "Display Name",
|
"label": "Anzeigename",
|
||||||
"placeholder": "The descriptive name for the interface"
|
"placeholder": "Der beschreibende Name für die Schnittstelle"
|
||||||
},
|
},
|
||||||
"private-key": {
|
"private-key": {
|
||||||
"label": "Private Key",
|
"label": "Privater Schlüssel",
|
||||||
"placeholder": "The private key"
|
"placeholder": "Der private Schlüssel"
|
||||||
},
|
},
|
||||||
"public-key": {
|
"public-key": {
|
||||||
"label": "Public Key",
|
"label": "Öffentlicher Schlüssel",
|
||||||
"placeholder": "The public key"
|
"placeholder": "Der öffentliche Schlüssel"
|
||||||
},
|
},
|
||||||
"ip": {
|
"ip": {
|
||||||
"label": "IP Addresses",
|
"label": "IP-Adressen",
|
||||||
"placeholder": "IP Addresses (CIDR format)"
|
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||||
},
|
},
|
||||||
"listen-port": {
|
"listen-port": {
|
||||||
"label": "Listen Port",
|
"label": "Port",
|
||||||
"placeholder": "The listening port"
|
"placeholder": "Der Port der WireGuard Schnittstelle"
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
"label": "DNS Server",
|
"label": "DNS-Server",
|
||||||
"placeholder": "The DNS servers that should be used"
|
"placeholder": "Die zu verwendenden DNS-Server"
|
||||||
},
|
},
|
||||||
"dns-search": {
|
"dns-search": {
|
||||||
"label": "DNS Search Domains",
|
"label": "DNS-Suchdomänen",
|
||||||
"placeholder": "DNS search prefixes"
|
"placeholder": "DNS-Suchpräfixe"
|
||||||
},
|
},
|
||||||
"mtu": {
|
"mtu": {
|
||||||
"label": "MTU",
|
"label": "MTU",
|
||||||
"placeholder": "The interface MTU (0 = keep default)"
|
"placeholder": "Die Schnittstellen-MTU (0 = Standard beibehalten)"
|
||||||
},
|
},
|
||||||
"firewall-mark": {
|
"firewall-mark": {
|
||||||
"label": "Firewall Mark",
|
"label": "Firewall-Markierung",
|
||||||
"placeholder": "Firewall mark that is applied to outgoing traffic. (0 = automatic)"
|
"placeholder": "Firewall-Markierung, die auf ausgehenden Datenverkehr angewendet wird. (0 = automatisch)"
|
||||||
},
|
},
|
||||||
"routing-table": {
|
"routing-table": {
|
||||||
"label": "Routing Table",
|
"label": "Routing-Tabelle",
|
||||||
"placeholder": "The routing table ID",
|
"placeholder": "Die Routing-Tabellen-ID",
|
||||||
"description": "Special cases: off = do not manage routes, 0 = automatic"
|
"description": "Spezialfälle: off = Routen nicht verwalten, 0 = automatisch"
|
||||||
},
|
},
|
||||||
"pre-up": {
|
"pre-up": {
|
||||||
"label": "Pre-Up",
|
"label": "Pre-Up",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"post-up": {
|
"post-up": {
|
||||||
"label": "Post-Up",
|
"label": "Post-Up",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"pre-down": {
|
"pre-down": {
|
||||||
"label": "Pre-Down",
|
"label": "Pre-Down",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"post-down": {
|
"post-down": {
|
||||||
"label": "Post-Down",
|
"label": "Post-Down",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"label": "Interface Disabled"
|
"label": "Schnittstelle deaktiviert"
|
||||||
},
|
},
|
||||||
"save-config": {
|
"save-config": {
|
||||||
"label": "Automatically save wg-quick config"
|
"label": "wg-quick Konfiguration automatisch speichern"
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "Endpoint Address",
|
"label": "Endpunktadresse",
|
||||||
"placeholder": "Endpoint Address",
|
"placeholder": "Endpunktadresse",
|
||||||
"description": "The endpoint address that peers will connect to. (e.g. wg.example.com or wg.example.com:51820)"
|
"description": "Die Endpunktadresse, mit der sich Peers verbinden. (z.B. wg.example.com oder wg.example.com:51820)"
|
||||||
},
|
},
|
||||||
"networks": {
|
"networks": {
|
||||||
"label": "IP Networks",
|
"label": "IP-Netzwerke",
|
||||||
"placeholder": "Network Addresses",
|
"placeholder": "Netzwerkadressen",
|
||||||
"description": "Peers will get IP addresses from those subnets."
|
"description": "Peers erhalten IP-Adressen aus diesen Subnetzen."
|
||||||
},
|
},
|
||||||
"allowed-ip": {
|
"allowed-ip": {
|
||||||
"label": "Allowed IP Addresses",
|
"label": "Erlaubte IP-Adressen",
|
||||||
"placeholder": "Default Allowed IP Addresses"
|
"placeholder": "Erlaubte IP-Adressen für Peers"
|
||||||
},
|
},
|
||||||
"mtu": {
|
"mtu": {
|
||||||
"label": "MTU",
|
"label": "MTU",
|
||||||
"placeholder": "The client MTU (0 = keep default)"
|
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||||
},
|
},
|
||||||
"keep-alive": {
|
"keep-alive": {
|
||||||
"label": "Keep Alive Interval",
|
"label": "Keepalive-Intervall",
|
||||||
"placeholder": "Persistent Keepalive (0 = default)"
|
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"button-apply-defaults": "Peer-Standardeinstellungen anwenden"
|
||||||
"button-apply-defaults": "Apply Peer Defaults"
|
|
||||||
},
|
},
|
||||||
"peer-view": {
|
"peer-view": {
|
||||||
"headline-peer": "Peer:",
|
"headline-peer": "Peer:",
|
||||||
"headline-endpoint": "Endpoint:",
|
"headline-endpoint": "Endpunkt:",
|
||||||
"section-info": "Peer Information",
|
"section-info": "Peer-Informationen",
|
||||||
"section-status": "Current Status",
|
"section-status": "Aktueller Status",
|
||||||
"section-config": "Configuration",
|
"section-config": "Konfiguration",
|
||||||
"identifier": "Identifier",
|
"identifier": "Kennung",
|
||||||
"ip": "IP Addresses",
|
"ip": "IP-Adressen",
|
||||||
"user": "Associated User",
|
"user": "Zugeordneter Benutzer",
|
||||||
"notes": "Notes",
|
"notes": "Notizen",
|
||||||
"expiry-status": "Expires At",
|
"expiry-status": "Läuft ab am",
|
||||||
"disabled-status": "Disabled At",
|
"disabled-status": "Deaktiviert am",
|
||||||
"traffic": "Traffic",
|
"traffic": "Datenverkehr",
|
||||||
"connection-status": "Connection Stats",
|
"connection-status": "Verbindungsstatistiken",
|
||||||
"upload": "Uploaded Bytes (from Server to Peer)",
|
"upload": "Hochgeladene Bytes (vom Server zum Peer)",
|
||||||
"download": "Downloaded Bytes (from Peer to Server)",
|
"download": "Heruntergeladene Bytes (vom Peer zum Server)",
|
||||||
"pingable": "Is Pingable",
|
"pingable": "Pingbar",
|
||||||
"handshake": "Last Handshake",
|
"handshake": "Letzter Handshake",
|
||||||
"connected-since": "Connected since",
|
"connected-since": "Verbunden seit",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpunkt",
|
||||||
"button-download": "Download configuration",
|
"button-download": "Konfiguration herunterladen",
|
||||||
"button-email": "Send configuration via E-Mail"
|
"button-email": "Konfiguration per E-Mail senden"
|
||||||
},
|
},
|
||||||
"peer-edit": {
|
"peer-edit": {
|
||||||
"headline-edit-peer": "Edit peer:",
|
"headline-edit-peer": "Peer bearbeiten:",
|
||||||
"headline-edit-endpoint": "Edit endpoint:",
|
"headline-edit-endpoint": "Endpunkt bearbeiten:",
|
||||||
"headline-new-peer": "Create peer",
|
"headline-new-peer": "Peer erstellen",
|
||||||
"headline-new-endpoint": "Create endpoint",
|
"headline-new-endpoint": "Endpunkt erstellen",
|
||||||
"header-general": "General",
|
"header-general": "Allgemein",
|
||||||
"header-network": "Network",
|
"header-network": "Netzwerk",
|
||||||
"header-crypto": "Cryptography",
|
"header-crypto": "Kryptografie",
|
||||||
"header-hooks": "Hooks (Executed on Peer)",
|
"header-hooks": "Hooks (beim Peer ausgeführt)",
|
||||||
"header-state": "State",
|
"header-state": "Status",
|
||||||
"display-name": {
|
"display-name": {
|
||||||
"label": "Display Name",
|
"label": "Anzeigename",
|
||||||
"placeholder": "The descriptive name for the peer"
|
"placeholder": "Der beschreibende Name für den Peer"
|
||||||
},
|
},
|
||||||
"linked-user": {
|
"linked-user": {
|
||||||
"label": "Linked User",
|
"label": "Verknüpfter Benutzer",
|
||||||
"placeholder": "The user account which owns this peer"
|
"placeholder": "Das Benutzerkonto, dem dieser Peer gehört"
|
||||||
},
|
},
|
||||||
"private-key": {
|
"private-key": {
|
||||||
"label": "Private Key",
|
"label": "Privater Schlüssel",
|
||||||
"placeholder": "The private key"
|
"placeholder": "Der private Schlüssel",
|
||||||
|
"help": "Der private Schlüssel wird sicher auf dem Server gespeichert. Wenn der Benutzer bereits eine Kopie besitzt, kann dieses Feld entfallen. Der Server funktioniert auch ausschließlich mit dem öffentlichen Schlüssel des Peers."
|
||||||
},
|
},
|
||||||
"public-key": {
|
"public-key": {
|
||||||
"label": "Public Key",
|
"label": "Öffentlicher Schlüssel",
|
||||||
"placeholder": "The public key"
|
"placeholder": "Der öffentliche Schlüssel"
|
||||||
},
|
},
|
||||||
"preshared-key": {
|
"preshared-key": {
|
||||||
"label": "Preshared Key",
|
"label": "Pre-Shared Key",
|
||||||
"placeholder": "Optional pre-shared key"
|
"placeholder": "Optionaler geteilter Schlüssel"
|
||||||
},
|
},
|
||||||
"endpoint-public-key": {
|
"endpoint-public-key": {
|
||||||
"label": "Endpoint public Key",
|
"label": "Öffentlicher Endpunktschlüssel",
|
||||||
"placeholder": "The public key of the remote endpoint"
|
"placeholder": "Der öffentliche Schlüssel des entfernten Endpunkts"
|
||||||
},
|
},
|
||||||
"endpoint": {
|
"endpoint": {
|
||||||
"label": "Endpoint Address",
|
"label": "Endpunktadresse",
|
||||||
"placeholder": "The address of the remote endpoint"
|
"placeholder": "Die Adresse des entfernten Endpunkts"
|
||||||
},
|
},
|
||||||
"ip": {
|
"ip": {
|
||||||
"label": "IP Addresses",
|
"label": "IP-Adressen",
|
||||||
"placeholder": "IP Addresses (CIDR format)"
|
"placeholder": "IP-Adressen (CIDR-Format)"
|
||||||
},
|
},
|
||||||
"allowed-ip": {
|
"allowed-ip": {
|
||||||
"label": "Allowed IP Addresses",
|
"label": "Erlaubte IP-Adressen",
|
||||||
"placeholder": "Allowed IP Addresses (CIDR format)"
|
"placeholder": "Erlaubte IP-Adressen (CIDR-Format)"
|
||||||
},
|
},
|
||||||
"extra-allowed-ip": {
|
"extra-allowed-ip": {
|
||||||
"label": "Extra allowed IP Addresses",
|
"label": "Zusätzliche erlaubte IP-Adressen",
|
||||||
"placeholder": "Extra allowed IP's (Server Sided)",
|
"placeholder": "Zusätzliche erlaubte IP's (Server-seitig)",
|
||||||
"description": "Those IP's will be added on the remote WireGuard interface as allowed IP's."
|
"description": "Diese IPs werden an der entfernten WireGuard-Schnittstelle als erlaubte IPs hinzugefügt."
|
||||||
},
|
},
|
||||||
"dns": {
|
"dns": {
|
||||||
"label": "DNS Server",
|
"label": "DNS-Server",
|
||||||
"placeholder": "The DNS servers that should be used"
|
"placeholder": "Die zu verwendenden DNS-Server"
|
||||||
},
|
},
|
||||||
"dns-search": {
|
"dns-search": {
|
||||||
"label": "DNS Search Domains",
|
"label": "DNS-Suchdomänen",
|
||||||
"placeholder": "DNS search prefixes"
|
"placeholder": "DNS-Suchpräfixe"
|
||||||
},
|
},
|
||||||
"keep-alive": {
|
"keep-alive": {
|
||||||
"label": "Keep Alive Interval",
|
"label": "Keepalive-Intervall",
|
||||||
"placeholder": "Persistent Keepalive (0 = default)"
|
"placeholder": "Persistentes Keepalive (0 = Standard)"
|
||||||
},
|
},
|
||||||
"mtu": {
|
"mtu": {
|
||||||
"label": "MTU",
|
"label": "MTU",
|
||||||
"placeholder": "The client MTU (0 = keep default)"
|
"placeholder": "Die Client-MTU (0 = Standard beibehalten)"
|
||||||
},
|
},
|
||||||
"pre-up": {
|
"pre-up": {
|
||||||
"label": "Pre-Up",
|
"label": "Pre-Up",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"post-up": {
|
"post-up": {
|
||||||
"label": "Post-Up",
|
"label": "Post-Up",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"pre-down": {
|
"pre-down": {
|
||||||
"label": "Pre-Down",
|
"label": "Pre-Down",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"post-down": {
|
"post-down": {
|
||||||
"label": "Post-Down",
|
"label": "Post-Down",
|
||||||
"placeholder": "One or multiple bash commands separated by ;"
|
"placeholder": "Ein oder mehrere Bash-Befehle, getrennt durch ;"
|
||||||
},
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"label": "Peer Disabled"
|
"label": "Peer deaktiviert"
|
||||||
},
|
},
|
||||||
"ignore-global": {
|
"ignore-global": {
|
||||||
"label": "Ignore global settings"
|
"label": "Globale Einstellungen ignorieren"
|
||||||
},
|
},
|
||||||
"expires-at": {
|
"expires-at": {
|
||||||
"label": "Expiry date"
|
"label": "Ablaufdatum"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peer-multi-create": {
|
"peer-multi-create": {
|
||||||
"headline-peer": "Create multiple peers",
|
"headline-peer": "Mehrere Peers erstellen",
|
||||||
"headline-endpoint": "Create multiple endpoints",
|
"headline-endpoint": "Mehrere Endpunkte erstellen",
|
||||||
"identifiers": {
|
"identifiers": {
|
||||||
"label": "User Identifiers",
|
"label": "Benutzerkennungen",
|
||||||
"placeholder": "User Identifiers",
|
"placeholder": "Benutzerkennungen",
|
||||||
"description": "A user identifier (the username) for which a peer should be created."
|
"description": "Eine Benutzerkennung (der Benutzername), für die ein Peer erstellt werden soll."
|
||||||
},
|
},
|
||||||
"prefix": {
|
"prefix": {
|
||||||
"headline-peer": "Peer:",
|
"headline-peer": "Peer:",
|
||||||
"headline-endpoint": "Endpoint:",
|
"headline-endpoint": "Endpunkt:",
|
||||||
"label": "Display Name Prefix",
|
"label": "Anzeigename-Präfix",
|
||||||
"placeholder": "The prefix",
|
"placeholder": "Das Präfix",
|
||||||
"description": "A prefix that is added to the peers display name."
|
"description": "Ein Präfix, das dem Anzeigenamen des Peers hinzugefügt wird."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
"label": "Password",
|
"label": "Password",
|
||||||
"placeholder": "Please enter your password"
|
"placeholder": "Please enter your password"
|
||||||
},
|
},
|
||||||
"button": "Sign in"
|
"button": "Sign in",
|
||||||
|
"button-webauthn": "Use Passkey"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@ -38,8 +39,10 @@
|
|||||||
"lang": "Toggle Language",
|
"lang": "Toggle Language",
|
||||||
"profile": "My Profile",
|
"profile": "My Profile",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"audit": "Audit Log",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"logout": "Logout"
|
"logout": "Logout",
|
||||||
|
"keygen": "Key Generator"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"headline": "WireGuard® VPN Portal",
|
"headline": "WireGuard® VPN Portal",
|
||||||
@ -186,6 +189,71 @@
|
|||||||
"button-enable-title": "Enable API, this will generate a new token.",
|
"button-enable-title": "Enable API, this will generate a new token.",
|
||||||
"button-enable-text": "Enable API",
|
"button-enable-text": "Enable API",
|
||||||
"api-link": "API Documentation"
|
"api-link": "API Documentation"
|
||||||
|
},
|
||||||
|
"webauthn": {
|
||||||
|
"headline": "Passkey Settings",
|
||||||
|
"abstract": "Passkeys are a modern way to authenticate users without the need for passwords. They are stored securely in your browser and can be used to log in to the WireGuard Portal.",
|
||||||
|
"active-description": "At least one passkey is currently active for your user account.",
|
||||||
|
"inactive-description": "No passkeys are currently registered for your user account. Press the button below to register a new passkey.",
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"created": "Created",
|
||||||
|
"actions": ""
|
||||||
|
},
|
||||||
|
"credentials-list": "Currently registered Passkeys",
|
||||||
|
"modal-delete": {
|
||||||
|
"headline": "Delete Passkey",
|
||||||
|
"abstract": "Are you sure you want to delete this passkey? You will not be able to log in with this passkey anymore.",
|
||||||
|
"created": "Created:",
|
||||||
|
"button-delete": "Delete",
|
||||||
|
"button-cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"button-rename-title": "Rename",
|
||||||
|
"button-rename-text": "Rename the passkey.",
|
||||||
|
"button-save-title": "Save",
|
||||||
|
"button-save-text": "Save the new name of the passkey.",
|
||||||
|
"button-cancel-title": "Cancel",
|
||||||
|
"button-cancel-text": "Cancel the renaming of the passkey.",
|
||||||
|
"button-delete-title": "Delete",
|
||||||
|
"button-delete-text": "Delete the passkey. You will not be able to log in with this passkey anymore.",
|
||||||
|
"button-register-title": "Register Passkey",
|
||||||
|
"button-register-text": "Register a new Passkey to secure your account."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "Audit Log",
|
||||||
|
"abstract": "Here you can find the audit log of all actions performed in the WireGuard Portal.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "No log entries available",
|
||||||
|
"abstract": "Currently, there are no audit logs recorded."
|
||||||
|
},
|
||||||
|
"entries-headline": "Log Entries",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "Time",
|
||||||
|
"user": "User",
|
||||||
|
"severity": "Severity",
|
||||||
|
"origin": "Origin",
|
||||||
|
"message": "Message"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keygen": {
|
||||||
|
"headline": "WireGuard Key Generator",
|
||||||
|
"abstract": "Generate a new WireGuard keys. The keys are generated in your local browser and are never sent to the server.",
|
||||||
|
"headline-keypair": "New Key Pair",
|
||||||
|
"headline-preshared-key": "New Preshared Key",
|
||||||
|
"button-generate": "Generate",
|
||||||
|
"private-key": {
|
||||||
|
"label": "Private Key",
|
||||||
|
"placeholder": "The private key"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Public Key",
|
||||||
|
"placeholder": "The public key"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "Preshared Key",
|
||||||
|
"placeholder": "The pre-shared key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"modals": {
|
"modals": {
|
||||||
@ -228,7 +296,8 @@
|
|||||||
"password": {
|
"password": {
|
||||||
"label": "Password",
|
"label": "Password",
|
||||||
"placeholder": "A super secret password",
|
"placeholder": "A super secret password",
|
||||||
"description": "Leave this field blank to keep current password."
|
"description": "Leave this field blank to keep current password.",
|
||||||
|
"too-weak": "The password is too weak. Please use a stronger password."
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"label": "Email",
|
"label": "Email",
|
||||||
@ -421,7 +490,8 @@
|
|||||||
},
|
},
|
||||||
"private-key": {
|
"private-key": {
|
||||||
"label": "Private Key",
|
"label": "Private Key",
|
||||||
"placeholder": "The private key"
|
"placeholder": "The private key",
|
||||||
|
"help": "The private key is stored securely on the server. If the user already holds a copy, you may omit this field. The server still functions exclusively with the peer’s public key."
|
||||||
},
|
},
|
||||||
"public-key": {
|
"public-key": {
|
||||||
"label": "Public Key",
|
"label": "Public Key",
|
||||||
|
532
frontend/src/lang/translations/ko.json
Normal file
532
frontend/src/lang/translations/ko.json
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
{
|
||||||
|
"languages": {
|
||||||
|
"ko": "한국어"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"pagination": {
|
||||||
|
"size": "항목 수",
|
||||||
|
"all": "전체 (느림)"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "검색...",
|
||||||
|
"button": "검색"
|
||||||
|
},
|
||||||
|
"select-all": "모두 선택",
|
||||||
|
"yes": "예",
|
||||||
|
"no": "아니오",
|
||||||
|
"cancel": "취소",
|
||||||
|
"close": "닫기",
|
||||||
|
"save": "저장",
|
||||||
|
"delete": "삭제"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"headline": "로그인하세요",
|
||||||
|
"username": {
|
||||||
|
"label": "사용자 이름",
|
||||||
|
"placeholder": "사용자 이름을 입력하세요"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "비밀번호",
|
||||||
|
"placeholder": "비밀번호를 입력하세요"
|
||||||
|
},
|
||||||
|
"button": "로그인"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"home": "홈",
|
||||||
|
"interfaces": "인터페이스",
|
||||||
|
"users": "사용자",
|
||||||
|
"lang": "언어 변경",
|
||||||
|
"profile": "내 프로필",
|
||||||
|
"settings": "설정",
|
||||||
|
"audit": "감사 로그",
|
||||||
|
"login": "로그인",
|
||||||
|
"logout": "로그아웃"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"headline": "WireGuard® VPN 포털",
|
||||||
|
"info-headline": "추가 정보",
|
||||||
|
"abstract": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다. IPsec보다 빠르고, 간단하며, 가볍고, 더 유용하면서도 엄청난 골칫거리를 피하는 것을 목표로 합니다. OpenVPN보다 훨씬 더 성능이 뛰어날 것으로 예상됩니다.",
|
||||||
|
"installation": {
|
||||||
|
"box-header": "WireGuard 설치",
|
||||||
|
"headline": "설치",
|
||||||
|
"content": "클라이언트 소프트웨어 설치 지침은 공식 WireGuard 웹사이트에서 찾을 수 있습니다.",
|
||||||
|
"button": "지침 열기"
|
||||||
|
},
|
||||||
|
"about-wg": {
|
||||||
|
"box-header": "WireGuard 정보",
|
||||||
|
"headline": "정보",
|
||||||
|
"content": "WireGuard®는 암호화 기술을 활용하는 매우 간단하면서도 빠르고 현대적인 VPN입니다.",
|
||||||
|
"button": "더 보기"
|
||||||
|
},
|
||||||
|
"about-portal": {
|
||||||
|
"box-header": "WireGuard 포털 정보",
|
||||||
|
"headline": "WireGuard 포털",
|
||||||
|
"content": "WireGuard 포털은 WireGuard를 위한 간단한 웹 기반 구성 포털입니다.",
|
||||||
|
"button": "더 보기"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"headline": "VPN 프로필",
|
||||||
|
"abstract": "사용자 프로필을 통해 개인 VPN 구성에 액세스하고 다운로드할 수 있습니다.",
|
||||||
|
"content": "구성된 모든 프로필을 찾으려면 아래 버튼을 클릭하세요.",
|
||||||
|
"button": "내 프로필 열기"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"headline": "관리 영역",
|
||||||
|
"abstract": "관리 영역에서는 WireGuard 피어 및 서버 인터페이스뿐만 아니라 WireGuard 포털에 로그인할 수 있는 사용자도 관리할 수 있습니다.",
|
||||||
|
"content": "",
|
||||||
|
"button-admin": "서버 관리 열기",
|
||||||
|
"button-user": "사용자 관리 열기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interfaces": {
|
||||||
|
"headline": "인터페이스 관리",
|
||||||
|
"headline-peers": "현재 VPN 피어",
|
||||||
|
"headline-endpoints": "현재 엔드포인트",
|
||||||
|
"no-interface": {
|
||||||
|
"default-selection": "사용 가능한 인터페이스 없음",
|
||||||
|
"headline": "인터페이스를 찾을 수 없습니다...",
|
||||||
|
"abstract": "새 WireGuard 인터페이스를 만들려면 위의 플러스 버튼을 클릭하세요."
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "사용 가능한 피어 없음",
|
||||||
|
"abstract": "현재 선택한 WireGuard 인터페이스에 사용 가능한 피어가 없습니다."
|
||||||
|
},
|
||||||
|
"table-heading": {
|
||||||
|
"name": "이름",
|
||||||
|
"user": "사용자",
|
||||||
|
"ip": "IP 주소",
|
||||||
|
"endpoint": "엔드포인트",
|
||||||
|
"status": "상태"
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"headline": "인터페이스 상태:",
|
||||||
|
"mode": "모드",
|
||||||
|
"key": "공개 키",
|
||||||
|
"endpoint": "공개 엔드포인트",
|
||||||
|
"port": "수신 포트",
|
||||||
|
"peers": "활성화된 피어",
|
||||||
|
"total-peers": "총 피어 수",
|
||||||
|
"endpoints": "활성화된 엔드포인트",
|
||||||
|
"total-endpoints": "총 엔드포인트 수",
|
||||||
|
"ip": "IP 주소",
|
||||||
|
"default-allowed-ip": "기본 허용 IP",
|
||||||
|
"dns": "DNS 서버",
|
||||||
|
"mtu": "MTU",
|
||||||
|
"default-keep-alive": "기본 Keepalive 간격",
|
||||||
|
"button-show-config": "구성 보기",
|
||||||
|
"button-download-config": "구성 다운로드",
|
||||||
|
"button-store-config": "wg-quick용 구성 저장",
|
||||||
|
"button-edit": "인터페이스 편집"
|
||||||
|
},
|
||||||
|
"button-add-interface": "인터페이스 추가",
|
||||||
|
"button-add-peer": "피어 추가",
|
||||||
|
"button-add-peers": "여러 피어 추가",
|
||||||
|
"button-show-peer": "피어 보기",
|
||||||
|
"button-edit-peer": "피어 편집",
|
||||||
|
"peer-disabled": "피어가 비활성화됨, 이유:",
|
||||||
|
"peer-expiring": "피어 만료 예정:",
|
||||||
|
"peer-connected": "연결됨",
|
||||||
|
"peer-not-connected": "연결되지 않음",
|
||||||
|
"peer-handshake": "마지막 핸드셰이크:"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"headline": "사용자 관리",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "ID",
|
||||||
|
"email": "이메일",
|
||||||
|
"firstname": "이름",
|
||||||
|
"lastname": "성",
|
||||||
|
"source": "소스",
|
||||||
|
"peers": "피어",
|
||||||
|
"admin": "관리자"
|
||||||
|
},
|
||||||
|
"no-user": {
|
||||||
|
"headline": "사용 가능한 사용자 없음",
|
||||||
|
"abstract": "현재 WireGuard 포털에 등록된 사용자가 없습니다."
|
||||||
|
},
|
||||||
|
"button-add-user": "사용자 추가",
|
||||||
|
"button-show-user": "사용자 보기",
|
||||||
|
"button-edit-user": "사용자 편집",
|
||||||
|
"user-disabled": "사용자가 비활성화됨, 이유:",
|
||||||
|
"user-locked": "계정이 잠김, 이유:",
|
||||||
|
"admin": "사용자에게 관리자 권한이 있습니다",
|
||||||
|
"no-admin": "사용자에게 관리자 권한이 없습니다"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"headline": "내 VPN 피어",
|
||||||
|
"table-heading": {
|
||||||
|
"name": "이름",
|
||||||
|
"ip": "IP 주소",
|
||||||
|
"stats": "상태",
|
||||||
|
"interface": "서버 인터페이스"
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "사용 가능한 피어 없음",
|
||||||
|
"abstract": "현재 사용자 프로필과 연결된 피어가 없습니다."
|
||||||
|
},
|
||||||
|
"peer-connected": "연결됨",
|
||||||
|
"button-add-peer": "피어 추가",
|
||||||
|
"button-show-peer": "피어 보기",
|
||||||
|
"button-edit-peer": "피어 편집"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "설정",
|
||||||
|
"abstract": "여기에서 개인 설정을 변경할 수 있습니다.",
|
||||||
|
"api": {
|
||||||
|
"headline": "API 설정",
|
||||||
|
"abstract": "여기에서 RESTful API 설정을 구성할 수 있습니다.",
|
||||||
|
"active-description": "현재 사용자 계정에 대해 API가 활성화되어 있습니다. 모든 API 요청은 기본 인증(Basic Auth)으로 인증됩니다. 인증에 다음 자격 증명을 사용하세요.",
|
||||||
|
"inactive-description": "현재 API가 비활성화되어 있습니다. 활성화하려면 아래 버튼을 누르세요.",
|
||||||
|
"user-label": "API 사용자 이름:",
|
||||||
|
"user-placeholder": "API 사용자",
|
||||||
|
"token-label": "API 비밀번호:",
|
||||||
|
"token-placeholder": "API 토큰",
|
||||||
|
"token-created-label": "API 액세스 권한 부여 시각: ",
|
||||||
|
"button-disable-title": "API를 비활성화합니다. 현재 토큰이 무효화됩니다.",
|
||||||
|
"button-disable-text": "API 비활성화",
|
||||||
|
"button-enable-title": "API를 활성화합니다. 새 토큰이 생성됩니다.",
|
||||||
|
"button-enable-text": "API 활성화",
|
||||||
|
"api-link": "API 문서"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "감사 로그",
|
||||||
|
"abstract": "여기에서 WireGuard 포털에서 수행된 모든 작업의 감사 로그를 찾을 수 있습니다.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "로그 항목 없음",
|
||||||
|
"abstract": "현재 기록된 감사 로그가 없습니다."
|
||||||
|
},
|
||||||
|
"entries-headline": "로그 항목",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "시간",
|
||||||
|
"user": "사용자",
|
||||||
|
"severity": "심각도",
|
||||||
|
"origin": "출처",
|
||||||
|
"message": "메시지"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"user-view": {
|
||||||
|
"headline": "사용자 계정:",
|
||||||
|
"tab-user": "정보",
|
||||||
|
"tab-peers": "피어",
|
||||||
|
"headline-info": "사용자 정보:",
|
||||||
|
"headline-notes": "메모:",
|
||||||
|
"email": "이메일",
|
||||||
|
"firstname": "이름",
|
||||||
|
"lastname": "성",
|
||||||
|
"phone": "전화번호",
|
||||||
|
"department": "부서",
|
||||||
|
"api-enabled": "API 액세스",
|
||||||
|
"disabled": "계정 비활성화됨",
|
||||||
|
"locked": "계정 잠김",
|
||||||
|
"no-peers": "사용자에게 연결된 피어가 없습니다.",
|
||||||
|
"peers": {
|
||||||
|
"name": "이름",
|
||||||
|
"interface": "인터페이스",
|
||||||
|
"ip": "IP 주소"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user-edit": {
|
||||||
|
"headline-edit": "사용자 편집:",
|
||||||
|
"headline-new": "새 사용자",
|
||||||
|
"header-general": "일반",
|
||||||
|
"header-personal": "사용자 정보",
|
||||||
|
"header-notes": "메모",
|
||||||
|
"header-state": "상태",
|
||||||
|
"identifier": {
|
||||||
|
"label": "식별자",
|
||||||
|
"placeholder": "고유한 사용자 식별자"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"label": "소스",
|
||||||
|
"placeholder": "사용자 소스"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "비밀번호",
|
||||||
|
"placeholder": "매우 비밀스러운 비밀번호",
|
||||||
|
"description": "현재 비밀번호를 유지하려면 이 필드를 비워 두세요."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"label": "이메일",
|
||||||
|
"placeholder": "이메일 주소"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"label": "전화번호",
|
||||||
|
"placeholder": "전화번호"
|
||||||
|
},
|
||||||
|
"department": {
|
||||||
|
"label": "부서",
|
||||||
|
"placeholder": "부서"
|
||||||
|
},
|
||||||
|
"firstname": {
|
||||||
|
"label": "이름",
|
||||||
|
"placeholder": "이름"
|
||||||
|
},
|
||||||
|
"lastname": {
|
||||||
|
"label": "성",
|
||||||
|
"placeholder": "성"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"label": "메모",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "비활성화됨 (WireGuard 연결 및 로그인 불가)"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"label": "잠김 (로그인 불가, WireGuard 연결은 계속 작동)"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"label": "관리자 여부"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interface-view": {
|
||||||
|
"headline": "인터페이스 구성:"
|
||||||
|
},
|
||||||
|
"interface-edit": {
|
||||||
|
"headline-edit": "인터페이스 편집:",
|
||||||
|
"headline-new": "새 인터페이스",
|
||||||
|
"tab-interface": "인터페이스",
|
||||||
|
"tab-peerdef": "피어 기본값",
|
||||||
|
"header-general": "일반",
|
||||||
|
"header-network": "네트워크",
|
||||||
|
"header-crypto": "암호화",
|
||||||
|
"header-hooks": "인터페이스 후크",
|
||||||
|
"header-peer-hooks": "후크",
|
||||||
|
"header-state": "상태",
|
||||||
|
"identifier": {
|
||||||
|
"label": "식별자",
|
||||||
|
"placeholder": "고유한 인터페이스 식별자"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "인터페이스 모드",
|
||||||
|
"server": "서버 모드",
|
||||||
|
"client": "클라이언트 모드",
|
||||||
|
"any": "알 수 없는 모드"
|
||||||
|
},
|
||||||
|
"display-name": {
|
||||||
|
"label": "표시 이름",
|
||||||
|
"placeholder": "인터페이스에 대한 설명적인 이름"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "개인 키",
|
||||||
|
"placeholder": "개인 키"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "공개 키",
|
||||||
|
"placeholder": "공개 키"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "IP 주소",
|
||||||
|
"placeholder": "IP 주소 (CIDR 형식)"
|
||||||
|
},
|
||||||
|
"listen-port": {
|
||||||
|
"label": "수신 포트",
|
||||||
|
"placeholder": "수신 포트"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "DNS 서버",
|
||||||
|
"placeholder": "사용해야 하는 DNS 서버"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "DNS 검색 도메인",
|
||||||
|
"placeholder": "DNS 검색 접두사"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "인터페이스 MTU (0 = 기본값 유지)"
|
||||||
|
},
|
||||||
|
"firewall-mark": {
|
||||||
|
"label": "방화벽 표시",
|
||||||
|
"placeholder": "나가는 트래픽에 적용되는 방화벽 표시. (0 = 자동)"
|
||||||
|
},
|
||||||
|
"routing-table": {
|
||||||
|
"label": "라우팅 테이블",
|
||||||
|
"placeholder": "라우팅 테이블 ID",
|
||||||
|
"description": "특수 사례: off = 경로 관리 안 함, 0 = 자동"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "인터페이스 비활성화됨"
|
||||||
|
},
|
||||||
|
"save-config": {
|
||||||
|
"label": "wg-quick 구성 자동 저장"
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"endpoint": {
|
||||||
|
"label": "엔드포인트 주소",
|
||||||
|
"placeholder": "엔드포인트 주소",
|
||||||
|
"description": "피어가 연결할 엔드포인트 주소. (예: wg.example.com 또는 wg.example.com:51820)"
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"label": "IP 네트워크",
|
||||||
|
"placeholder": "네트워크 주소",
|
||||||
|
"description": "피어는 해당 서브넷에서 IP 주소를 받습니다."
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "허용된 IP 주소",
|
||||||
|
"placeholder": "기본 허용 IP 주소"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Keep Alive 간격",
|
||||||
|
"placeholder": "영구 Keepalive (0 = 기본값)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button-apply-defaults": "피어 기본값 적용"
|
||||||
|
},
|
||||||
|
"peer-view": {
|
||||||
|
"headline-peer": "피어:",
|
||||||
|
"headline-endpoint": "엔드포인트:",
|
||||||
|
"section-info": "피어 정보",
|
||||||
|
"section-status": "현재 상태",
|
||||||
|
"section-config": "구성",
|
||||||
|
"identifier": "식별자",
|
||||||
|
"ip": "IP 주소",
|
||||||
|
"user": "연결된 사용자",
|
||||||
|
"notes": "메모",
|
||||||
|
"expiry-status": "만료 시각",
|
||||||
|
"disabled-status": "비활성화 시각",
|
||||||
|
"traffic": "트래픽",
|
||||||
|
"connection-status": "연결 통계",
|
||||||
|
"upload": "업로드된 바이트 (서버에서 피어로)",
|
||||||
|
"download": "다운로드된 바이트 (피어에서 서버로)",
|
||||||
|
"pingable": "핑 가능 여부",
|
||||||
|
"handshake": "마지막 핸드셰이크",
|
||||||
|
"connected-since": "연결 시작 시각",
|
||||||
|
"endpoint": "엔드포인트",
|
||||||
|
"button-download": "구성 다운로드",
|
||||||
|
"button-email": "이메일로 구성 보내기"
|
||||||
|
},
|
||||||
|
"peer-edit": {
|
||||||
|
"headline-edit-peer": "피어 편집:",
|
||||||
|
"headline-edit-endpoint": "엔드포인트 편집:",
|
||||||
|
"headline-new-peer": "피어 생성",
|
||||||
|
"headline-new-endpoint": "엔드포인트 생성",
|
||||||
|
"header-general": "일반",
|
||||||
|
"header-network": "네트워크",
|
||||||
|
"header-crypto": "암호화",
|
||||||
|
"header-hooks": "후크 (피어에서 실행됨)",
|
||||||
|
"header-state": "상태",
|
||||||
|
"display-name": {
|
||||||
|
"label": "표시 이름",
|
||||||
|
"placeholder": "피어에 대한 설명적인 이름"
|
||||||
|
},
|
||||||
|
"linked-user": {
|
||||||
|
"label": "연결된 사용자",
|
||||||
|
"placeholder": "이 피어를 소유한 사용자 계정"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "개인 키",
|
||||||
|
"placeholder": "개인 키"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "공개 키",
|
||||||
|
"placeholder": "공개 키"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "사전 공유 키",
|
||||||
|
"placeholder": "선택적 사전 공유 키"
|
||||||
|
},
|
||||||
|
"endpoint-public-key": {
|
||||||
|
"label": "엔드포인트 공개 키",
|
||||||
|
"placeholder": "원격 엔드포인트의 공개 키"
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"label": "엔드포인트 주소",
|
||||||
|
"placeholder": "원격 엔드포인트의 주소"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "IP 주소",
|
||||||
|
"placeholder": "IP 주소 (CIDR 형식)"
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "허용된 IP 주소",
|
||||||
|
"placeholder": "허용된 IP 주소 (CIDR 형식)"
|
||||||
|
},
|
||||||
|
"extra-allowed-ip": {
|
||||||
|
"label": "추가 허용 IP 주소",
|
||||||
|
"placeholder": "추가 허용 IP (서버 측)",
|
||||||
|
"description": "이 IP 주소는 원격 WireGuard 인터페이스에 허용된 IP로 추가됩니다."
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "DNS 서버",
|
||||||
|
"placeholder": "사용해야 하는 DNS 서버"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "DNS 검색 도메인",
|
||||||
|
"placeholder": "DNS 검색 접두사"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Keep Alive 간격",
|
||||||
|
"placeholder": "영구 Keepalive (0 = 기본값)"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "클라이언트 MTU (0 = 기본값 유지)"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "하나 이상의 bash 명령 (;으로 구분)"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "피어 비활성화됨"
|
||||||
|
},
|
||||||
|
"ignore-global": {
|
||||||
|
"label": "전역 설정 무시"
|
||||||
|
},
|
||||||
|
"expires-at": {
|
||||||
|
"label": "만료 날짜"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peer-multi-create": {
|
||||||
|
"headline-peer": "여러 피어 생성",
|
||||||
|
"headline-endpoint": "여러 엔드포인트 생성",
|
||||||
|
"identifiers": {
|
||||||
|
"label": "사용자 식별자",
|
||||||
|
"placeholder": "사용자 식별자",
|
||||||
|
"description": "피어를 생성할 사용자 식별자 (사용자 이름)."
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"headline-peer": "피어:",
|
||||||
|
"headline-endpoint": "엔드포인트:",
|
||||||
|
"label": "표시 이름 접두사",
|
||||||
|
"placeholder": "접두사",
|
||||||
|
"description": "피어 표시 이름에 추가되는 접두사."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
552
frontend/src/lang/translations/pt.json
Normal file
552
frontend/src/lang/translations/pt.json
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
{
|
||||||
|
"languages": {
|
||||||
|
"pt": "Português"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"pagination": {
|
||||||
|
"size": "Número de Elementos",
|
||||||
|
"all": "Todos (lento)"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Pesquisar...",
|
||||||
|
"button": "Pesquisar"
|
||||||
|
},
|
||||||
|
"select-all": "Selecionar tudo",
|
||||||
|
"yes": "Sim",
|
||||||
|
"no": "Não",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"close": "Fechar",
|
||||||
|
"save": "Guardar",
|
||||||
|
"delete": "Eliminar"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"headline": "Por favor, inicie a sessão",
|
||||||
|
"username": {
|
||||||
|
"label": "Nome de utilizador",
|
||||||
|
"placeholder": "Por favor, insira o seu nome de utilizador"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Palavra-passe",
|
||||||
|
"placeholder": "Por favor, insira a sua palavra-passe"
|
||||||
|
},
|
||||||
|
"button": "Iniciar sessão"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"home": "Início",
|
||||||
|
"interfaces": "Interfaces",
|
||||||
|
"users": "Utilizadores",
|
||||||
|
"lang": "Alterar idioma",
|
||||||
|
"profile": "O Meu Perfil",
|
||||||
|
"settings": "Definições",
|
||||||
|
"audit": "Registo de Auditoria",
|
||||||
|
"login": "Iniciar Sessão",
|
||||||
|
"logout": "Terminar Sessão",
|
||||||
|
"keygen": "Gerador de Chave"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"headline": "WireGuard® Portal VPN",
|
||||||
|
"info-headline": "Mais Informações",
|
||||||
|
"abstract": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração. O seu objetivo é ser mais rápida, simples, leve e útil que o IPsec, enquanto evita grandes dores de cabeça. Pretende ser consideravelmente mais eficiente que o OpenVPN.",
|
||||||
|
"installation": {
|
||||||
|
"box-header": "Instalação do WireGuard",
|
||||||
|
"headline": "Instalação",
|
||||||
|
"content": "As instruções de instalação para o software cliente podem ser encontradas no site oficial do WireGuard.",
|
||||||
|
"button": "Abrir Instruções"
|
||||||
|
},
|
||||||
|
"about-wg": {
|
||||||
|
"box-header": "Sobre o WireGuard",
|
||||||
|
"headline": "Sobre",
|
||||||
|
"content": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração.",
|
||||||
|
"button": "Mais"
|
||||||
|
},
|
||||||
|
"about-portal": {
|
||||||
|
"box-header": "Sobre o WireGuard Portal",
|
||||||
|
"headline": "WireGuard Portal",
|
||||||
|
"content": "WireGuard Portal é um portal web de configuração simples para o WireGuard.",
|
||||||
|
"button": "Mais"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"headline": "Perfis VPN",
|
||||||
|
"abstract": "Pode aceder e baixar as suas configurações pessoais de VPN através do seu Perfil de Utilizador.",
|
||||||
|
"content": "Para encontrar todos os seus perfis configurados, clique no botão abaixo.",
|
||||||
|
"button": "Abrir meu perfil"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"headline": "Área de Administração",
|
||||||
|
"abstract": "Na área de administração, pode gerir os peers do WireGuard, a interface do servidor e os utilizadores que têm permissão para aceder ao Portal WireGuard.",
|
||||||
|
"content": "",
|
||||||
|
"button-admin": "Abrir Administração do Servidor",
|
||||||
|
"button-user": "Abrir Administração de Utilizadores"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interfaces": {
|
||||||
|
"headline": "Administração de Interfaces",
|
||||||
|
"headline-peers": "Peers VPN Atuais",
|
||||||
|
"headline-endpoints": "Endpoints Atuais",
|
||||||
|
"no-interface": {
|
||||||
|
"default-selection": "Nenhuma interface disponível",
|
||||||
|
"headline": "Nenhuma interface encontrada...",
|
||||||
|
"abstract": "Clique no botão + acima para criar uma nova interface WireGuard."
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "Nenhum peer disponível",
|
||||||
|
"abstract": "Atualmente, não há peers disponíveis para a interface WireGuard selecionada."
|
||||||
|
},
|
||||||
|
"table-heading": {
|
||||||
|
"name": "Nome",
|
||||||
|
"user": "Utilizador",
|
||||||
|
"ip": "IPs",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"status": "Status"
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"headline": "Status da interface para",
|
||||||
|
"mode": "modo",
|
||||||
|
"key": "Chave Pública",
|
||||||
|
"endpoint": "Endpoint Público",
|
||||||
|
"port": "Porta de Escuta",
|
||||||
|
"peers": "Peers Ativados",
|
||||||
|
"total-peers": "Total de Peers",
|
||||||
|
"endpoints": "Endpoints Ativados",
|
||||||
|
"total-endpoints": "Total de Endpoints",
|
||||||
|
"ip": "Endereço IP",
|
||||||
|
"default-allowed-ip": "IPs permitidos por padrão",
|
||||||
|
"dns": "Servidores DNS",
|
||||||
|
"mtu": "MTU",
|
||||||
|
"default-keep-alive": "Intervalo de Keepalive Padrão",
|
||||||
|
"button-show-config": "Mostrar configuração",
|
||||||
|
"button-download-config": "Baixar configuração",
|
||||||
|
"button-store-config": "Armazenar configuração para wg-quick",
|
||||||
|
"button-edit": "Editar interface"
|
||||||
|
},
|
||||||
|
"button-add-interface": "Adicionar Interface",
|
||||||
|
"button-add-peer": "Adicionar Peer",
|
||||||
|
"button-add-peers": "Adicionar Vários Peers",
|
||||||
|
"button-show-peer": "Mostrar Peer",
|
||||||
|
"button-edit-peer": "Editar Peer",
|
||||||
|
"peer-disabled": "Peer desativado, razão:",
|
||||||
|
"peer-expiring": "Peer expira em",
|
||||||
|
"peer-connected": "Conectado",
|
||||||
|
"peer-not-connected": "Não Conectado",
|
||||||
|
"peer-handshake": "Último handshake:"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"headline": "Administração de Utilizadores",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "ID",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"firstname": "Primeiro Nome",
|
||||||
|
"lastname": "Último Nome",
|
||||||
|
"source": "Fonte",
|
||||||
|
"peers": "Peers",
|
||||||
|
"admin": "Administrador"
|
||||||
|
},
|
||||||
|
"no-user": {
|
||||||
|
"headline": "Nenhum utilizador disponível",
|
||||||
|
"abstract": "Atualmente, não há utilizadores registados no Portal WireGuard."
|
||||||
|
},
|
||||||
|
"button-add-user": "Adicionar Utilizador",
|
||||||
|
"button-show-user": "Mostrar Utilizador",
|
||||||
|
"button-edit-user": "Editar Utilizador",
|
||||||
|
"user-disabled": "Utilizador desativado, razão:",
|
||||||
|
"user-locked": "Conta bloqueada, razão:",
|
||||||
|
"admin": "O utilizador tem privilégios de administrador",
|
||||||
|
"no-admin": "O utilizador não tem privilégios de administrador"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"headline": "Os Meus Peers VPN",
|
||||||
|
"table-heading": {
|
||||||
|
"name": "Nome",
|
||||||
|
"ip": "IPs",
|
||||||
|
"stats": "Status",
|
||||||
|
"interface": "Interface do Servidor"
|
||||||
|
},
|
||||||
|
"no-peer": {
|
||||||
|
"headline": "Nenhum peer disponível",
|
||||||
|
"abstract": "Atualmente, não há peers associados ao seu perfil de utilizador."
|
||||||
|
},
|
||||||
|
"peer-connected": "Conectado",
|
||||||
|
"button-add-peer": "Adicionar Peer",
|
||||||
|
"button-show-peer": "Mostrar Peer",
|
||||||
|
"button-edit-peer": "Editar Peer"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"headline": "Definições",
|
||||||
|
"abstract": "Aqui pode alterar suas Definições pessoais.",
|
||||||
|
"api": {
|
||||||
|
"headline": "Definições da API",
|
||||||
|
"abstract": "Aqui pode configurar as definições da API RESTful.",
|
||||||
|
"active-description": "A API está atualmente ativa para a sua conta de utilizador. Todos os pedidos para a API são autenticadas com Basic Auth. Use as seguintes credenciais para autenticação.",
|
||||||
|
"inactive-description": "A API está atualmente inativa. Pressione o botão abaixo para ativá-la.",
|
||||||
|
"user-label": "Nome de utilizador API:",
|
||||||
|
"user-placeholder": "O utilizador da API",
|
||||||
|
"token-label": "Senha da API:",
|
||||||
|
"token-placeholder": "O token da API",
|
||||||
|
"token-created-label": "Acesso API concedido em: ",
|
||||||
|
"button-disable-title": "Desativar API, invalidando o token atual.",
|
||||||
|
"button-disable-text": "Desativar API",
|
||||||
|
"button-enable-title": "Ativar API, gerando um novo token.",
|
||||||
|
"button-enable-text": "Ativar API",
|
||||||
|
"api-link": "Documentação da API"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audit": {
|
||||||
|
"headline": "Registo de Auditoria",
|
||||||
|
"abstract": "Aqui pode encontrar o registo de auditoria de todas as ações realizadas no WireGuard Portal.",
|
||||||
|
"no-entries": {
|
||||||
|
"headline": "Nenhuma entrada no registo",
|
||||||
|
"abstract": "Atualmente, não há entradas de registo de auditoria gravadas."
|
||||||
|
},
|
||||||
|
"entries-headline": "Entradas do Registo",
|
||||||
|
"table-heading": {
|
||||||
|
"id": "#",
|
||||||
|
"time": "Hora",
|
||||||
|
"user": "Utilizador",
|
||||||
|
"severity": "Gravidade",
|
||||||
|
"origin": "Origem",
|
||||||
|
"message": "Mensagem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keygen": {
|
||||||
|
"headline": "Gerador de Chaves WireGuard",
|
||||||
|
"abstract": "Gere novas chaves WireGuard. As chaves são geradas no seu browser e nunca são enviadas para o servidor.",
|
||||||
|
"headline-keypair": "Novo Par de Chaves",
|
||||||
|
"headline-preshared-key": "Nova Chave Pré-Partilhada",
|
||||||
|
"button-generate": "Gerar",
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Chave Pública",
|
||||||
|
"placeholder": "A chave pública"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "Chave Pré-Partilhada",
|
||||||
|
"placeholder": "A chave pré-partilhada"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modals": {
|
||||||
|
"user-view": {
|
||||||
|
"headline": "Conta de Utilizador:",
|
||||||
|
"tab-user": "Informação",
|
||||||
|
"tab-peers": "Peers",
|
||||||
|
"headline-info": "Informação do Utilizador:",
|
||||||
|
"headline-notes": "Notas:",
|
||||||
|
"email": "E-Mail",
|
||||||
|
"firstname": "Primeiro Nome",
|
||||||
|
"lastname": "Último Nome",
|
||||||
|
"phone": "Número de Telefone",
|
||||||
|
"department": "Departamento",
|
||||||
|
"api-enabled": "Acesso API",
|
||||||
|
"disabled": "Conta Desativada",
|
||||||
|
"locked": "Conta Bloqueada",
|
||||||
|
"no-peers": "O utilizador não tem peers associados.",
|
||||||
|
"peers": {
|
||||||
|
"name": "Nome",
|
||||||
|
"interface": "Interface",
|
||||||
|
"ip": "IP's"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user-edit": {
|
||||||
|
"headline-edit": "Editar utilizador:",
|
||||||
|
"headline-new": "Novo utilizador",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-personal": "Informação do Utilizador",
|
||||||
|
"header-notes": "Notas",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"identifier": {
|
||||||
|
"label": "Identificador",
|
||||||
|
"placeholder": "O identificador único do utilizador"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"label": "Fonte",
|
||||||
|
"placeholder": "A fonte do utilizador"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Palavra-passe",
|
||||||
|
"placeholder": "Uma palavra-passe super secreta",
|
||||||
|
"description": "Deixe este campo em branco para manter a palavra-passe atual."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"label": "Email",
|
||||||
|
"placeholder": "O endereço de e-mail"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"label": "Telefone",
|
||||||
|
"placeholder": "O número de telefone"
|
||||||
|
},
|
||||||
|
"department": {
|
||||||
|
"label": "Departamento",
|
||||||
|
"placeholder": "O departamento"
|
||||||
|
},
|
||||||
|
"firstname": {
|
||||||
|
"label": "Primeiro Nome",
|
||||||
|
"placeholder": "Primeiro Nome"
|
||||||
|
},
|
||||||
|
"lastname": {
|
||||||
|
"label": "Último Nome",
|
||||||
|
"placeholder": "Último Nome"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"label": "Notas",
|
||||||
|
"placeholder": ""
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Desativado (sem conexão WireGuard e login possível)"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"label": "Bloqueado (sem login possível, as conexões WireGuard ainda funcionam)"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"label": "É Administrador"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interface-view": {
|
||||||
|
"headline": "Configuração para a Interface:"
|
||||||
|
},
|
||||||
|
"interface-edit": {
|
||||||
|
"headline-edit": "Editar Interface:",
|
||||||
|
"headline-new": "Nova Interface",
|
||||||
|
"tab-interface": "Interface",
|
||||||
|
"tab-peerdef": "Padrões de Peer",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-network": "Rede",
|
||||||
|
"header-crypto": "Criptografia",
|
||||||
|
"header-hooks": "Hooks da Interface",
|
||||||
|
"header-peer-hooks": "Hooks",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"identifier": {
|
||||||
|
"label": "Identificador",
|
||||||
|
"placeholder": "O identificador único da interface"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Modo da Interface",
|
||||||
|
"server": "Modo Servidor",
|
||||||
|
"client": "Modo Cliente",
|
||||||
|
"any": "Modo Desconhecido"
|
||||||
|
},
|
||||||
|
"display-name": {
|
||||||
|
"label": "Nome de Exibição",
|
||||||
|
"placeholder": "O nome descritivo para a interface"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Chave Pública",
|
||||||
|
"placeholder": "A chave pública"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "Endereços IP",
|
||||||
|
"placeholder": "Endereços IP (formato CIDR)"
|
||||||
|
},
|
||||||
|
"listen-port": {
|
||||||
|
"label": "Porta de Escuta",
|
||||||
|
"placeholder": "A porta de escuta"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "Servidor DNS",
|
||||||
|
"placeholder": "Os servidores DNS que devem ser usados"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "Domínios de Pesquisa DNS",
|
||||||
|
"placeholder": "Prefixos de pesquisa DNS"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU da interface (0 = manter o valor padrão)"
|
||||||
|
},
|
||||||
|
"firewall-mark": {
|
||||||
|
"label": "Marca de Firewall",
|
||||||
|
"placeholder": "Marca de firewall aplicada ao tráfego de saída. (0 = automático)"
|
||||||
|
},
|
||||||
|
"routing-table": {
|
||||||
|
"label": "Tabela de Roteamento",
|
||||||
|
"placeholder": "O ID da tabela de roteamento",
|
||||||
|
"description": "Casos especiais: off = não gerenciar rotas, 0 = automático"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Interface Desativada"
|
||||||
|
},
|
||||||
|
"save-config": {
|
||||||
|
"label": "Guardar configuração wg-quick automaticamente"
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"endpoint": {
|
||||||
|
"label": "Endereço do Endpoint",
|
||||||
|
"placeholder": "Endereço do Endpoint",
|
||||||
|
"description": "O endereço do endpoint ao qual os peers se irão conectar. (ex. wg.exemplo.com ou wg.exemplo.com:51820)"
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"label": "Redes IP",
|
||||||
|
"placeholder": "Endereços de Rede",
|
||||||
|
"description": "Os peers irão obter endereços IP a partir dessas sub-redes."
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "Endereços IP Permitidos",
|
||||||
|
"placeholder": "Endereços IP Permitidos por padrão"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU do cliente (0 = manter o valor padrão)"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Intervalo de Keep Alive",
|
||||||
|
"placeholder": "Keepalive persistente (0 = padrão)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"button-apply-defaults": "Aplicar Padrões de Peer"
|
||||||
|
},
|
||||||
|
"peer-view": {
|
||||||
|
"headline-peer": "Peer:",
|
||||||
|
"headline-endpoint": "Endpoint:",
|
||||||
|
"section-info": "Informação do Peer",
|
||||||
|
"section-status": "Estado Atual",
|
||||||
|
"section-config": "Configuração",
|
||||||
|
"identifier": "Identificador",
|
||||||
|
"ip": "Endereços IP",
|
||||||
|
"user": "Utilizador Associado",
|
||||||
|
"notes": "Notas",
|
||||||
|
"expiry-status": "Expira em",
|
||||||
|
"disabled-status": "Desativado em",
|
||||||
|
"traffic": "Tráfego",
|
||||||
|
"connection-status": "Estatísticas de Conexão",
|
||||||
|
"upload": "Bytes Enviados (do Servidor para o Peer)",
|
||||||
|
"download": "Bytes Recebidos (do Peer para o Servidor)",
|
||||||
|
"pingable": "É Pingável",
|
||||||
|
"handshake": "Último Handshake",
|
||||||
|
"connected-since": "Conectado desde",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"button-download": "Baixar configuração",
|
||||||
|
"button-email": "Enviar configuração por E-Mail"
|
||||||
|
},
|
||||||
|
"peer-edit": {
|
||||||
|
"headline-edit-peer": "Editar peer:",
|
||||||
|
"headline-edit-endpoint": "Editar endpoint:",
|
||||||
|
"headline-new-peer": "Criar peer",
|
||||||
|
"headline-new-endpoint": "Criar endpoint",
|
||||||
|
"header-general": "Geral",
|
||||||
|
"header-network": "Rede",
|
||||||
|
"header-crypto": "Criptografia",
|
||||||
|
"header-hooks": "Hooks (Executados no Peer)",
|
||||||
|
"header-state": "Estado",
|
||||||
|
"display-name": {
|
||||||
|
"label": "Nome de Exibição",
|
||||||
|
"placeholder": "O nome descritivo para o peer"
|
||||||
|
},
|
||||||
|
"linked-user": {
|
||||||
|
"label": "Utilizador Associado",
|
||||||
|
"placeholder": "A conta de utilizador que possui este peer"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"label": "Chave Privada",
|
||||||
|
"placeholder": "A chave privada",
|
||||||
|
"help": "A chave privada é armazenada de forma segura no servidor. Se o utilizador já tiver uma cópia, pode omitir este campo. O servidor ainda funciona exclusivamente com a chave pública do peer."
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"label": "Chave Pública",
|
||||||
|
"placeholder": "A chave pública"
|
||||||
|
},
|
||||||
|
"preshared-key": {
|
||||||
|
"label": "Chave Pré-Partilhada",
|
||||||
|
"placeholder": "Chave pré-partilhada opcional"
|
||||||
|
},
|
||||||
|
"endpoint-public-key": {
|
||||||
|
"label": "Chave Pública do Endpoint",
|
||||||
|
"placeholder": "A chave pública do endpoint remoto"
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"label": "Endereço do Endpoint",
|
||||||
|
"placeholder": "O endereço do endpoint remoto"
|
||||||
|
},
|
||||||
|
"ip": {
|
||||||
|
"label": "Endereços IP",
|
||||||
|
"placeholder": "Endereços IP (formato CIDR)"
|
||||||
|
},
|
||||||
|
"allowed-ip": {
|
||||||
|
"label": "Endereços IP Permitidos",
|
||||||
|
"placeholder": "Endereços IP permitidos"
|
||||||
|
},
|
||||||
|
"extra-allowed-ip": {
|
||||||
|
"label": "Endereços IP adicionais permitidos",
|
||||||
|
"placeholder": "IPs adicionais permitidos (lado do servidor)",
|
||||||
|
"description": "Esses IPs serão adicionados à interface WireGuard remota como IPs permitidos."
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"label": "Servidor DNS",
|
||||||
|
"placeholder": "Os servidores DNS que devem ser utilizados"
|
||||||
|
},
|
||||||
|
"dns-search": {
|
||||||
|
"label": "Domínios de Pesquisa DNS",
|
||||||
|
"placeholder": "Prefixos de pesquisa DNS"
|
||||||
|
},
|
||||||
|
"keep-alive": {
|
||||||
|
"label": "Intervalo de Keep Alive",
|
||||||
|
"placeholder": "Keepalive persistente (0 = padrão)"
|
||||||
|
},
|
||||||
|
"mtu": {
|
||||||
|
"label": "MTU",
|
||||||
|
"placeholder": "O MTU do cliente (0 = manter o padrão)"
|
||||||
|
},
|
||||||
|
"pre-up": {
|
||||||
|
"label": "Pre-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-up": {
|
||||||
|
"label": "Post-Up",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"pre-down": {
|
||||||
|
"label": "Pre-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"post-down": {
|
||||||
|
"label": "Post-Down",
|
||||||
|
"placeholder": "Um ou vários comandos bash separados por ;"
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"label": "Peer Desativado"
|
||||||
|
},
|
||||||
|
"ignore-global": {
|
||||||
|
"label": "Ignorar definições globais"
|
||||||
|
},
|
||||||
|
"expires-at": {
|
||||||
|
"label": "Data de expiração"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peer-multi-create": {
|
||||||
|
"headline-peer": "Criar múltiplos peers",
|
||||||
|
"headline-endpoint": "Criar múltiplos endpoints",
|
||||||
|
"identifiers": {
|
||||||
|
"label": "Identificadores de utilizador",
|
||||||
|
"placeholder": "Identificadores de utilizador",
|
||||||
|
"description": "Um identificador de utilizador (nome de utilizador) para o qual um peer deve ser criado."
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
"headline-peer": "Peer:",
|
||||||
|
"headline-endpoint": "Endpoint:",
|
||||||
|
"label": "Prefixo do nome exibido",
|
||||||
|
"placeholder": "O prefixo",
|
||||||
|
"description": "Um prefixo que será adicionado ao nome exibido do peer."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import LoginView from '../views/LoginView.vue'
|
|||||||
import InterfaceView from '../views/InterfaceView.vue'
|
import InterfaceView from '../views/InterfaceView.vue'
|
||||||
|
|
||||||
import {authStore} from '@/stores/auth'
|
import {authStore} from '@/stores/auth'
|
||||||
|
import {securityStore} from '@/stores/security'
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -55,6 +56,22 @@ const router = createRouter({
|
|||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/SettingsView.vue')
|
component: () => import('../views/SettingsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/audit',
|
||||||
|
name: 'audit',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import('../views/AuditView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/key-generator',
|
||||||
|
name: 'key-generator',
|
||||||
|
// route level code-splitting
|
||||||
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// which is lazy-loaded when the route is visited.
|
||||||
|
component: () => import('../views/KeyGeneraterView.vue')
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
linkActiveClass: "active",
|
linkActiveClass: "active",
|
||||||
@ -105,13 +122,22 @@ router.beforeEach(async (to) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// redirect to login page if not logged in and trying to access a restricted page
|
// redirect to login page if not logged in and trying to access a restricted page
|
||||||
const publicPages = ['/', '/login']
|
const publicPages = ['/', '/login', '/key-generator']
|
||||||
const authRequired = !publicPages.includes(to.path)
|
const authRequired = !publicPages.includes(to.path)
|
||||||
|
|
||||||
if (authRequired && !auth.IsAuthenticated) {
|
if (authRequired && !auth.IsAuthenticated) {
|
||||||
auth.SetReturnUrl(to.fullPath) // store original destination before starting the auth process
|
auth.SetReturnUrl(to.fullPath) // store the original destination before starting the auth process
|
||||||
return '/login'
|
return '/login'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.afterEach(async (to, from) => {
|
||||||
|
const sec = securityStore()
|
||||||
|
const csrfPages = ['/', '/login']
|
||||||
|
|
||||||
|
if (csrfPages.includes(to.path)) {
|
||||||
|
await sec.LoadSecurityProperties() // make sure we have a valid csrf token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
87
frontend/src/stores/audit.js
Normal file
87
frontend/src/stores/audit.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
import { base64_url_encode } from '@/helpers/encoding';
|
||||||
|
|
||||||
|
const baseUrl = `/audit`
|
||||||
|
|
||||||
|
export const auditStore = defineStore('audit', {
|
||||||
|
state: () => ({
|
||||||
|
entries: [],
|
||||||
|
filter: "",
|
||||||
|
pageSize: 10,
|
||||||
|
pageOffset: 0,
|
||||||
|
pages: [],
|
||||||
|
fetching: false,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
Count: (state) => state.entries.length,
|
||||||
|
FilteredCount: (state) => state.Filtered.length,
|
||||||
|
All: (state) => state.entries,
|
||||||
|
Filtered: (state) => {
|
||||||
|
if (!state.filter) {
|
||||||
|
return state.entries
|
||||||
|
}
|
||||||
|
return state.entries.filter((e) => {
|
||||||
|
return e.Timestamp.includes(state.filter) ||
|
||||||
|
e.Message.includes(state.filter) ||
|
||||||
|
e.Severity.includes(state.filter) ||
|
||||||
|
e.Origin.includes(state.filter)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
FilteredAndPaged: (state) => {
|
||||||
|
return state.Filtered.slice(state.pageOffset, state.pageOffset + state.pageSize)
|
||||||
|
},
|
||||||
|
isFetching: (state) => state.fetching,
|
||||||
|
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||||
|
hasPrevPage: (state) => state.pageOffset > 0,
|
||||||
|
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
afterPageSizeChange() {
|
||||||
|
// reset pageOffset to avoid problems with new page sizes
|
||||||
|
this.pageOffset = 0
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
calculatePages() {
|
||||||
|
let pageCounter = 1;
|
||||||
|
this.pages = []
|
||||||
|
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
|
||||||
|
this.pages.push(pageCounter++)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gotoPage(page) {
|
||||||
|
this.pageOffset = (page-1) * this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
previousPage() {
|
||||||
|
this.pageOffset -= this.pageSize
|
||||||
|
|
||||||
|
this.calculatePages()
|
||||||
|
},
|
||||||
|
setEntries(entries) {
|
||||||
|
this.entries = entries
|
||||||
|
this.calculatePages()
|
||||||
|
this.fetching = false
|
||||||
|
},
|
||||||
|
async LoadEntries() {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.get(`${baseUrl}/entries`)
|
||||||
|
.then(this.setEntries)
|
||||||
|
.catch(error => {
|
||||||
|
this.setEntries([])
|
||||||
|
console.log("Failed to load audit entries: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to load audit entries!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
@ -3,14 +3,17 @@ import { defineStore } from 'pinia'
|
|||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
|
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||||
|
import {base64_url_encode} from "@/helpers/encoding";
|
||||||
|
|
||||||
export const authStore = defineStore({
|
export const authStore = defineStore('auth',{
|
||||||
id: 'auth',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// initialize state from local storage to enable user to stay logged in
|
// initialize state from local storage to enable user to stay logged in
|
||||||
user: JSON.parse(localStorage.getItem('user')),
|
user: JSON.parse(localStorage.getItem('user')),
|
||||||
providers: [],
|
providers: [],
|
||||||
returnUrl: localStorage.getItem('returnUrl')
|
returnUrl: localStorage.getItem('returnUrl'),
|
||||||
|
webAuthnCredentials: [],
|
||||||
|
fetching: false,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
UserIdentifier: (state) => state.user?.Identifier || 'unknown',
|
||||||
@ -19,6 +22,14 @@ export const authStore = defineStore({
|
|||||||
IsAuthenticated: (state) => state.user != null,
|
IsAuthenticated: (state) => state.user != null,
|
||||||
IsAdmin: (state) => state.user?.IsAdmin || false,
|
IsAdmin: (state) => state.user?.IsAdmin || false,
|
||||||
ReturnUrl: (state) => state.returnUrl || '/',
|
ReturnUrl: (state) => state.returnUrl || '/',
|
||||||
|
IsWebAuthnEnabled: (state) => {
|
||||||
|
if (state.webAuthnCredentials) {
|
||||||
|
return state.webAuthnCredentials.length > 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
WebAuthnCredentials: (state) => state.webAuthnCredentials || [],
|
||||||
|
isFetching: (state) => state.fetching,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
SetReturnUrl(link) {
|
SetReturnUrl(link) {
|
||||||
@ -61,6 +72,23 @@ export const authStore = defineStore({
|
|||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
// LoadWebAuthnCredentials returns promise that might have been rejected if the session was not authenticated.
|
||||||
|
async LoadWebAuthnCredentials() {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.get(`/auth/webauthn/credentials`)
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.setWebAuthnCredentials([])
|
||||||
|
console.log("Failed to load webauthn credentials:", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: error,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
// Login returns promise that might have been rejected if the login attempt was not successful.
|
// Login returns promise that might have been rejected if the login attempt was not successful.
|
||||||
async Login(username, password) {
|
async Login(username, password) {
|
||||||
return apiWrapper.post(`/auth/login`, { username, password })
|
return apiWrapper.post(`/auth/login`, { username, password })
|
||||||
@ -94,6 +122,157 @@ export const authStore = defineStore({
|
|||||||
|
|
||||||
await router.push('/login')
|
await router.push('/login')
|
||||||
},
|
},
|
||||||
|
async RegisterWebAuthn() {
|
||||||
|
// check if the browser supports WebAuthn
|
||||||
|
if (!browserSupportsWebAuthn()) {
|
||||||
|
console.error("WebAuthn is not supported by this browser.");
|
||||||
|
notify({
|
||||||
|
title: "WebAuthn not supported",
|
||||||
|
text: "This browser does not support WebAuthn.",
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(new Error("WebAuthn not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetching = true
|
||||||
|
console.log("Starting WebAuthn registration...")
|
||||||
|
await apiWrapper.post(`/auth/webauthn/register/start`, {})
|
||||||
|
.then(optionsJSON => {
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration",
|
||||||
|
text: "Starting passkey registration, follow the instructions in the browser."
|
||||||
|
});
|
||||||
|
console.log("Started WebAuthn registration with options: ", optionsJSON)
|
||||||
|
|
||||||
|
return startRegistration({ optionsJSON: optionsJSON.publicKey }).then(attResp => {
|
||||||
|
console.log("Finishing WebAuthn registration...")
|
||||||
|
return apiWrapper.post(`/auth/webauthn/register/finish`, attResp)
|
||||||
|
.then(credentials => {
|
||||||
|
console.log("Passkey registration finished successfully: ", credentials)
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration",
|
||||||
|
text: "A new passkey has been registered successfully!",
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to register passkey:", err);
|
||||||
|
notify({
|
||||||
|
title: "Passkey registration failed",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to start WebAuthn registration:", err);
|
||||||
|
notify({
|
||||||
|
title: "Failed to start Passkey registration",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to start WebAuthn registration:", err);
|
||||||
|
notify({
|
||||||
|
title: "Failed to start WebAuthn registration",
|
||||||
|
text: err,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async DeleteWebAuthnCredential(credentialId) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.delete(`/auth/webauthn/credential/${base64_url_encode(credentialId)}`)
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Success",
|
||||||
|
text: "Passkey deleted successfully!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to delete webauthn credential:", err);
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async RenameWebAuthnCredential(credential) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.put(`/auth/webauthn/credential/${base64_url_encode(credential.ID)}`, {
|
||||||
|
Name: credential.Name,
|
||||||
|
})
|
||||||
|
.then(credentials => {
|
||||||
|
this.setWebAuthnCredentials(credentials)
|
||||||
|
notify({
|
||||||
|
title: "Success",
|
||||||
|
text: "Passkey renamed successfully!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.fetching = false
|
||||||
|
console.error("Failed to rename webauthn credential", credential.ID, ":", err);
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async LoginWebAuthn() {
|
||||||
|
// check if the browser supports WebAuthn
|
||||||
|
if (!browserSupportsWebAuthn()) {
|
||||||
|
console.error("WebAuthn is not supported by this browser.");
|
||||||
|
notify({
|
||||||
|
title: "WebAuthn not supported",
|
||||||
|
text: "This browser does not support WebAuthn.",
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
return Promise.reject(new Error("WebAuthn not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetching = true
|
||||||
|
console.log("Starting WebAuthn login...")
|
||||||
|
await apiWrapper.post(`/auth/webauthn/login/start`, {})
|
||||||
|
.then(optionsJSON => {
|
||||||
|
console.log("Started WebAuthn login with options: ", optionsJSON)
|
||||||
|
|
||||||
|
return startAuthentication({ optionsJSON: optionsJSON.publicKey }).then(asseResp => {
|
||||||
|
console.log("Finishing WebAuthn login ...")
|
||||||
|
return apiWrapper.post(`/auth/webauthn/login/finish`, asseResp)
|
||||||
|
.then(user => {
|
||||||
|
console.log("Passkey login finished successfully for user:", user.Identifier)
|
||||||
|
this.ResetReturnUrl()
|
||||||
|
this.setUserInfo(user)
|
||||||
|
return user.Identifier
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to login with passkey:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Failed to finish passkey login:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to start passkey login:", err)
|
||||||
|
this.setUserInfo(null)
|
||||||
|
return Promise.reject(new Error("login failed"))
|
||||||
|
})
|
||||||
|
},
|
||||||
// -- internal setters
|
// -- internal setters
|
||||||
setUserInfo(userInfo) {
|
setUserInfo(userInfo) {
|
||||||
// store user details and jwt in local storage to keep user logged in between page refreshes
|
// store user details and jwt in local storage to keep user logged in between page refreshes
|
||||||
@ -121,5 +300,9 @@ export const authStore = defineStore({
|
|||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setWebAuthnCredentials(credentials) {
|
||||||
|
this.fetching = false
|
||||||
|
this.webAuthnCredentials = credentials
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -7,8 +7,7 @@ import { base64_url_encode } from '@/helpers/encoding';
|
|||||||
|
|
||||||
const baseUrl = `/interface`
|
const baseUrl = `/interface`
|
||||||
|
|
||||||
export const interfaceStore = defineStore({
|
export const interfaceStore = defineStore('interfaces', {
|
||||||
id: 'interfaces',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
prepared: freshInterface(),
|
prepared: freshInterface(),
|
||||||
|
@ -8,8 +8,7 @@ import { ipToBigInt } from '@/helpers/utils';
|
|||||||
|
|
||||||
const baseUrl = `/peer`
|
const baseUrl = `/peer`
|
||||||
|
|
||||||
export const peerStore = defineStore({
|
export const peerStore = defineStore('peers', {
|
||||||
id: 'peers',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
stats: {},
|
stats: {},
|
||||||
|
@ -8,8 +8,7 @@ import { ipToBigInt } from '@/helpers/utils';
|
|||||||
|
|
||||||
const baseUrl = `/user`
|
const baseUrl = `/user`
|
||||||
|
|
||||||
export const profileStore = defineStore({
|
export const profileStore = defineStore('profile', {
|
||||||
id: 'profile',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
interfaces: [],
|
interfaces: [],
|
||||||
@ -130,7 +129,7 @@ export const profileStore = defineStore({
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to activate API for ", currentUser, ": ", error)
|
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
@ -144,7 +143,7 @@ export const profileStore = defineStore({
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
|
@ -3,8 +3,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
|
|
||||||
export const securityStore = defineStore({
|
export const securityStore = defineStore('security',{
|
||||||
id: 'security',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
csrfToken: "",
|
csrfToken: "",
|
||||||
}),
|
}),
|
||||||
|
@ -5,8 +5,7 @@ import { apiWrapper } from '@/helpers/fetch-wrapper'
|
|||||||
|
|
||||||
const baseUrl = `/config`
|
const baseUrl = `/config`
|
||||||
|
|
||||||
export const settingsStore = defineStore({
|
export const settingsStore = defineStore('settings', {
|
||||||
id: 'settings',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
settings: {},
|
settings: {},
|
||||||
}),
|
}),
|
||||||
|
@ -5,8 +5,7 @@ import { base64_url_encode } from '@/helpers/encoding';
|
|||||||
|
|
||||||
const baseUrl = `/user`
|
const baseUrl = `/user`
|
||||||
|
|
||||||
export const userStore = defineStore({
|
export const userStore = defineStore('users', {
|
||||||
id: 'users',
|
|
||||||
state: () => ({
|
state: () => ({
|
||||||
userPeers: [],
|
userPeers: [],
|
||||||
users: [],
|
users: [],
|
||||||
|
96
frontend/src/views/AuditView.vue
Normal file
96
frontend/src/views/AuditView.vue
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import {auditStore} from "@/stores/audit";
|
||||||
|
|
||||||
|
const audit = auditStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await audit.LoadEntries()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('audit.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('audit.abstract') }}</p>
|
||||||
|
|
||||||
|
<!-- Entry list -->
|
||||||
|
<div class="mt-4 row">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<h3>{{ $t('audit.entries-headline') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-6 text-lg-end">
|
||||||
|
<div class="form-group d-inline">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input v-model="audit.filter" class="form-control" :placeholder="$t('general.search.placeholder')" type="text" @keyup="audit.afterPageSizeChange">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('general.search.button')"><i class="fa-solid fa-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 table-responsive">
|
||||||
|
<div v-if="audit.Count===0">
|
||||||
|
<h4>{{ $t('audit.no-entries.headline') }}</h4>
|
||||||
|
<p>{{ $t('audit.no-entries.abstract') }}</p>
|
||||||
|
</div>
|
||||||
|
<table v-if="audit.Count!==0" id="auditTable" class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.id') }}</th>
|
||||||
|
<th class="text-center" scope="col">{{ $t('audit.table-heading.time') }}</th>
|
||||||
|
<th class="text-center" scope="col">{{ $t('audit.table-heading.severity') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.user') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.origin') }}</th>
|
||||||
|
<th scope="col">{{ $t('audit.table-heading.message') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="entry in audit.FilteredAndPaged" :key="entry.Id">
|
||||||
|
<td>{{entry.Id}}</td>
|
||||||
|
<td>{{entry.Timestamp}}</td>
|
||||||
|
<td class="text-center"><span class="badge rounded-pill" :class="[ entry.Severity === 'low' ? 'bg-light' : entry.Severity === 'medium' ? 'bg-warning' : 'bg-danger']">{{entry.Severity}}</span></td>
|
||||||
|
<td>{{entry.ContextUser}}</td>
|
||||||
|
<td>{{entry.Origin}}</td>
|
||||||
|
<td>{{entry.Message}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<ul class="pagination pagination-sm">
|
||||||
|
<li :class="{disabled:audit.pageOffset===0}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.previousPage">«</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-for="page in audit.pages" :key="page" :class="{active:audit.currentPage===page}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.gotoPage(page)">{{page}}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li :class="{disabled:!audit.hasNextPage}" class="page-item">
|
||||||
|
<a class="page-link" @click="audit.nextPage">»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -65,7 +65,7 @@ const auth = authStore()
|
|||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<h4 class="card-title">{{ $t('home.about-portal.headline') }}</h4>
|
<h4 class="card-title">{{ $t('home.about-portal.headline') }}</h4>
|
||||||
<p class="card-text">{{ $t('home.about-portal.content') }}</p>
|
<p class="card-text">{{ $t('home.about-portal.content') }}</p>
|
||||||
<a href="https://github.com/h44z/wg-portal/" title="WireGuard Portal" target="_blank"
|
<a href="https://wgportal.org/" title="WireGuard Portal" target="_blank"
|
||||||
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-portal.button') }}</a>
|
rel="noopener noreferrer" class="mt-auto btn btn-primary btn-sm">{{ $t('home.about-portal.button') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,8 +22,9 @@ const multiCreatePeerId = ref("")
|
|||||||
const editInterfaceId = ref("")
|
const editInterfaceId = ref("")
|
||||||
const viewedInterfaceId = ref("")
|
const viewedInterfaceId = ref("")
|
||||||
|
|
||||||
const sortKey = ref("");
|
const sortKey = ref("")
|
||||||
const sortOrder = ref(1);
|
const sortOrder = ref(1)
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
@ -48,12 +49,11 @@ async function download() {
|
|||||||
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier)
|
||||||
|
|
||||||
// credit: https://www.bitdegree.org/learn/javascript-download
|
// credit: https://www.bitdegree.org/learn/javascript-download
|
||||||
let filename = interfaces.GetSelected.Identifier + ".conf"
|
|
||||||
let text = interfaces.configuration
|
let text = interfaces.configuration
|
||||||
|
|
||||||
let element = document.createElement('a')
|
let element = document.createElement('a')
|
||||||
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
element.setAttribute('href', 'data:application/octet-stream;charset=utf-8,' + encodeURIComponent(text))
|
||||||
element.setAttribute('download', filename)
|
element.setAttribute('download', interfaces.GetSelected.Filename)
|
||||||
|
|
||||||
element.style.display = 'none'
|
element.style.display = 'none'
|
||||||
document.body.appendChild(element)
|
document.body.appendChild(element)
|
||||||
@ -81,6 +81,12 @@ async function saveConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
peers.FilteredAndPaged.forEach(peer => {
|
||||||
|
peer.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await interfaces.LoadInterfaces()
|
await interfaces.LoadInterfaces()
|
||||||
await peers.LoadPeers(undefined) // use default interface
|
await peers.LoadPeers(undefined) // use default interface
|
||||||
@ -326,7 +332,7 @@ onMounted(async () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col" @click="sortBy('DisplayName')">
|
<th scope="col" @click="sortBy('DisplayName')">
|
||||||
@ -357,7 +363,7 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
<tr v-for="peer in peers.FilteredAndPaged" :key="peer.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
<span v-if="peer.Disabled" class="text-danger" :title="$t('interfaces.peer-disabled') + ' ' + peer.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||||
@ -410,7 +416,7 @@ onMounted(async () => {
|
|||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
|
147
frontend/src/views/KeyGeneraterView.vue
Normal file
147
frontend/src/views/KeyGeneraterView.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
const privateKey = ref("")
|
||||||
|
const publicKey = ref("")
|
||||||
|
const presharedKey = ref("")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an X25519 keypair using the Web Crypto API and return Base64-encoded strings.
|
||||||
|
* @async
|
||||||
|
* @function generateKeypair
|
||||||
|
* @returns {Promise<{ publicKey: string, privateKey: string }>} Resolves with an object containing
|
||||||
|
* - publicKey: the Base64-encoded public key
|
||||||
|
* - privateKey: the Base64-encoded private key
|
||||||
|
*/
|
||||||
|
async function generateKeypair() {
|
||||||
|
// 1. Generate an X25519 key pair
|
||||||
|
const keyPair = await crypto.subtle.generateKey(
|
||||||
|
{ name: 'X25519', namedCurve: 'X25519' },
|
||||||
|
true, // extractable
|
||||||
|
['deriveBits'] // allowed usage for ECDH
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Export keys as JWK to access raw key material
|
||||||
|
const pubJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey);
|
||||||
|
const privJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);
|
||||||
|
|
||||||
|
// 3. Convert Base64URL to standard Base64 with padding
|
||||||
|
return {
|
||||||
|
publicKey: b64urlToB64(pubJwk.x),
|
||||||
|
privateKey: b64urlToB64(privJwk.d)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a 32-byte pre-shared key using crypto.getRandomValues.
|
||||||
|
* @function generatePresharedKey
|
||||||
|
* @returns {Uint8Array} A Uint8Array of length 32 with random bytes.
|
||||||
|
*/
|
||||||
|
function generatePresharedKey() {
|
||||||
|
let privateKey = new Uint8Array(32);
|
||||||
|
window.crypto.getRandomValues(privateKey);
|
||||||
|
return privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Base64URL-encoded string to standard Base64 with padding.
|
||||||
|
* @function b64urlToB64
|
||||||
|
* @param {string} input - The Base64URL string.
|
||||||
|
* @returns {string} The padded, standard Base64 string.
|
||||||
|
*/
|
||||||
|
function b64urlToB64(input) {
|
||||||
|
let b64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
while (b64.length % 4) {
|
||||||
|
b64 += '=';
|
||||||
|
}
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an ArrayBuffer or TypedArray buffer to a Base64-encoded string.
|
||||||
|
* @function arrayBufferToBase64
|
||||||
|
* @param {ArrayBuffer|Uint8Array} buffer - The buffer to convert.
|
||||||
|
* @returns {string} Base64-encoded representation of the buffer.
|
||||||
|
*/
|
||||||
|
function arrayBufferToBase64(buffer) {
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.byteLength; ++i) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
// Window.btoa handles binary → Base64
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new keypair and update the corresponding Vue refs.
|
||||||
|
* @async
|
||||||
|
* @function generateNewKeyPair
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function generateNewKeyPair() {
|
||||||
|
const keypair = await generateKeypair();
|
||||||
|
|
||||||
|
privateKey.value = keypair.privateKey;
|
||||||
|
publicKey.value = keypair.publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new pre-shared key and update the Vue ref.
|
||||||
|
* @function generateNewPresharedKey
|
||||||
|
*/
|
||||||
|
function generateNewPresharedKey() {
|
||||||
|
const rawPsk = generatePresharedKey();
|
||||||
|
presharedKey.value = arrayBufferToBase64(rawPsk);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>{{ $t('keygen.headline') }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="lead">{{ $t('keygen.abstract') }}</p>
|
||||||
|
|
||||||
|
<div class="mt-4 row">
|
||||||
|
<div class="col-12 col-lg-5">
|
||||||
|
<h1>{{ $t('keygen.headline-keypair') }}</h1>
|
||||||
|
<fieldset>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('keygen.private-key.label') }}</label>
|
||||||
|
<input class="form-control" v-model="privateKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('keygen.public-key.label') }}</label>
|
||||||
|
<input class="form-control" v-model="publicKey" :placeholder="$t('keygen.private-key.placeholder')" readonly>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<hr class="mt-4">
|
||||||
|
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewKeyPair">{{ $t('keygen.button-generate') }}</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-2 mt-sm-4">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-5">
|
||||||
|
<h1>{{ $t('keygen.headline-preshared-key') }}</h1>
|
||||||
|
<fieldset>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label mt-4">{{ $t('keygen.preshared-key.label') }}</label>
|
||||||
|
<input class="form-control" v-model="presharedKey" :placeholder="$t('keygen.preshared-key.placeholder')" readonly>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<hr class="mt-4">
|
||||||
|
<button class="btn btn-primary mb-4" type="button" @click.prevent="generateNewPresharedKey">{{ $t('keygen.button-generate') }}</button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import {computed, ref} from "vue";
|
import {computed, onMounted, ref} from "vue";
|
||||||
import {authStore} from "@/stores/auth";
|
import {authStore} from "@/stores/auth";
|
||||||
import router from '../router/index.js'
|
import router from '../router/index.js'
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
@ -16,6 +16,14 @@ const password = ref("")
|
|||||||
const usernameInvalid = computed(() => username.value === "")
|
const usernameInvalid = computed(() => username.value === "")
|
||||||
const passwordInvalid = computed(() => password.value === "")
|
const passwordInvalid = computed(() => password.value === "")
|
||||||
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
const disableLoginBtn = computed(() => username.value === "" || password.value === "" || loggingIn.value)
|
||||||
|
const showLoginForm = computed(() => {
|
||||||
|
console.log(router.currentRoute.value.query)
|
||||||
|
return settings.Setting('LoginFormVisible') || router.currentRoute.value.query.hasOwnProperty('all');
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await settings.LoadSettings()
|
||||||
|
})
|
||||||
|
|
||||||
const login = async function () {
|
const login = async function () {
|
||||||
console.log("Performing login for user:", username.value);
|
console.log("Performing login for user:", username.value);
|
||||||
@ -28,7 +36,34 @@ const login = async function () {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
loggingIn.value = false;
|
loggingIn.value = false;
|
||||||
settings.LoadSettings(); // only logs errors, does not throw
|
settings.LoadSettings(); // reload full settings
|
||||||
|
router.push(auth.ReturnUrl);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notify({
|
||||||
|
title: "Login failed!",
|
||||||
|
text: "Authentication failed!",
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
|
||||||
|
//loggingIn.value = false;
|
||||||
|
// delay the user from logging in for a short amount of time
|
||||||
|
setTimeout(() => loggingIn.value = false, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginWebAuthn = async function () {
|
||||||
|
console.log("Performing webauthn login");
|
||||||
|
loggingIn.value = true;
|
||||||
|
auth.LoginWebAuthn()
|
||||||
|
.then(uid => {
|
||||||
|
notify({
|
||||||
|
title: "Logged in",
|
||||||
|
text: "Authentication succeeded!",
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
loggingIn.value = false;
|
||||||
|
settings.LoadSettings(); // reload full settings
|
||||||
router.push(auth.ReturnUrl);
|
router.push(auth.ReturnUrl);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -66,7 +101,7 @@ const externalLogin = function (provider) {
|
|||||||
</div></div>
|
</div></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<fieldset>
|
<fieldset v-if="showLoginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
<label class="form-label" for="inputUsername">{{ $t('login.username.label') }}</label>
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
@ -85,13 +120,42 @@ const externalLogin = function (provider) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-5 d-flex">
|
<div class="row mt-5 mb-2">
|
||||||
<div :class="{'col-lg-4':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
<div class="col-sm-4 col-xs-12">
|
||||||
<button :disabled="disableLoginBtn" class="btn btn-primary flex-fill" type="submit" @click.prevent="login">
|
<button :disabled="disableLoginBtn" class="btn btn-primary mb-2" type="submit" @click.prevent="login">
|
||||||
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
{{ $t('login.button') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{'col-lg-8':auth.LoginProviders.length < 3, 'col-lg-12':auth.LoginProviders.length >= 3}" class="d-flex mb-2">
|
<div class="col-sm-8 col-xs-12 text-sm-end">
|
||||||
|
<button v-if="settings.Setting('WebAuthnEnabled')" class="btn btn-primary" type="submit" @click.prevent="loginWebAuthn">
|
||||||
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4 d-flex">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
|
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||||
|
v-html="provider.Name" @click.prevent="externalLogin(provider)"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset v-else>
|
||||||
|
<div class="row mt-1 mb-2" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
|
<button class="btn btn-outline-primary flex-fill" type="submit" @click.prevent="loginWebAuthn">
|
||||||
|
{{ $t('login.button-webauthn') }} <div v-if="loggingIn" class="d-inline"><i class="ms-2 fa-solid fa-circle-notch fa-spin"></i></div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-1 d-flex">
|
||||||
|
<div class="col-lg-12 d-flex mb-2">
|
||||||
<!-- OpenIdConnect / OAUTH providers -->
|
<!-- OpenIdConnect / OAUTH providers -->
|
||||||
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
<button v-for="(provider, idx) in auth.LoginProviders" :key="provider.Identifier" :class="{'ms-1':idx > 0}"
|
||||||
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
:disabled="loggingIn" :title="provider.Name" class="btn btn-outline-primary flex-fill"
|
||||||
@ -104,7 +168,6 @@ const externalLogin = function (provider) {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,8 +13,9 @@ const profile = profileStore()
|
|||||||
const viewedPeerId = ref("")
|
const viewedPeerId = ref("")
|
||||||
const editPeerId = ref("")
|
const editPeerId = ref("")
|
||||||
|
|
||||||
const sortKey = ref("");
|
const sortKey = ref("")
|
||||||
const sortOrder = ref(1);
|
const sortOrder = ref(1)
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
@ -34,6 +35,12 @@ function friendlyInterfaceName(id, name) {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
profile.FilteredAndPagedPeers.forEach(peer => {
|
||||||
|
peer.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
await profile.LoadPeers()
|
await profile.LoadPeers()
|
||||||
@ -86,8 +93,7 @@ onMounted(async () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox"
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
value="">
|
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col" @click="sortBy('DisplayName')">
|
<th scope="col" @click="sortBy('DisplayName')">
|
||||||
@ -112,7 +118,7 @@ onMounted(async () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
<tr v-for="peer in profile.FilteredAndPagedPeers" :key="peer.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="peer.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark"
|
<span v-if="peer.Disabled" class="text-danger"><i class="fa fa-circle-xmark"
|
||||||
@ -172,7 +178,7 @@ onMounted(async () => {
|
|||||||
{{ $t('general.pagination.size')}}:
|
{{ $t('general.pagination.size')}}:
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
import {onMounted, ref} from "vue";
|
||||||
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
import PeerEditModal from "@/components/PeerEditModal.vue";
|
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { humanFileSize } from "@/helpers/utils";
|
import { authStore } from "../stores/auth";
|
||||||
import {RouterLink} from "vue-router";
|
|
||||||
import {authStore} from "../stores/auth";
|
|
||||||
|
|
||||||
const profile = profileStore()
|
const profile = profileStore()
|
||||||
const settings = settingsStore()
|
const settings = settingsStore()
|
||||||
@ -15,8 +10,30 @@ const auth = authStore()
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
|
await auth.LoadWebAuthnCredentials()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedCredential = ref({})
|
||||||
|
|
||||||
|
function enableRename(credential) {
|
||||||
|
credential.renameMode = true;
|
||||||
|
credential.tempName = credential.Name; // Store the original name
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRename(credential) {
|
||||||
|
credential.renameMode = false;
|
||||||
|
credential.tempName = null; // Discard changes
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRename(credential) {
|
||||||
|
try {
|
||||||
|
await auth.RenameWebAuthnCredential({ ...credential, Name: credential.tempName });
|
||||||
|
credential.Name = credential.tempName; // Update the name
|
||||||
|
credential.renameMode = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to rename credential:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -74,4 +91,86 @@ onMounted(async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-light p-5 mt-5" v-if="settings.Setting('WebAuthnEnabled')">
|
||||||
|
<h2 class="display-7">{{ $t('settings.webauthn.headline') }}</h2>
|
||||||
|
<p class="lead">{{ $t('settings.webauthn.abstract') }}</p>
|
||||||
|
<hr class="my-4">
|
||||||
|
<p v-if="auth.IsWebAuthnEnabled">{{ $t('settings.webauthn.active-description') }}</p>
|
||||||
|
<p v-else>{{ $t('settings.webauthn.inactive-description') }}</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<button class="input-group-text btn btn-primary" :title="$t('settings.webauthn.button-register-text')" @click.prevent="auth.RegisterWebAuthn" :disabled="auth.isFetching">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.webauthn.button-register-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="auth.WebAuthnCredentials.length > 0" class="mt-4">
|
||||||
|
<h3>{{ $t('settings.webauthn.credentials-list') }}</h3>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 50%">{{ $t('settings.webauthn.table.name') }}</th>
|
||||||
|
<th style="width: 20%">{{ $t('settings.webauthn.table.created') }}</th>
|
||||||
|
<th style="width: 30%">{{ $t('settings.webauthn.table.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="credential in auth.webAuthnCredentials" :key="credential.ID">
|
||||||
|
<td class="align-middle">
|
||||||
|
<div v-if="credential.renameMode">
|
||||||
|
<input v-model="credential.tempName" class="form-control" type="text" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ credential.Name }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
|
{{ credential.CreatedAt }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle text-center">
|
||||||
|
<div v-if="credential.renameMode">
|
||||||
|
<button class="btn btn-success me-1" :title="$t('settings.webauthn.button-save-text')" @click.prevent="saveRename(credential)" :disabled="auth.isFetching">
|
||||||
|
{{ $t('settings.webauthn.button-save-title') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" :title="$t('settings.webauthn.button-cancel-text')" @click.prevent="cancelRename(credential)">
|
||||||
|
{{ $t('settings.webauthn.button-cancel-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<button class="btn btn-secondary me-1" :title="$t('settings.webauthn.button-rename-text')" @click.prevent="enableRename(credential)">
|
||||||
|
{{ $t('settings.webauthn.button-rename-title') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" :title="$t('settings.webauthn.button-delete-text')" data-bs-toggle="modal" data-bs-target="#webAuthnDeleteModal" :disabled="auth.isFetching" @click="selectedCredential=credential">
|
||||||
|
{{ $t('settings.webauthn.button-delete-title') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="webAuthnDeleteModal" tabindex="-1" aria-labelledby="webAuthnDeleteModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-white">
|
||||||
|
<h5 class="modal-title" id="webAuthnDeleteModalLabel">{{ $t('settings.webauthn.modal-delete.headline') }}</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" :aria-label="$t('settings.webauthn.modal-delete.button-cancel')"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h5 class="mb-3">{{ selectedCredential.Name }} <small class="text-body-secondary">({{ $t('settings.webauthn.modal-delete.created') }} {{ selectedCredential.CreatedAt }})</small></h5>
|
||||||
|
<p class="mb-0">{{ $t('settings.webauthn.modal-delete.abstract') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-cancel') }}</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmWebAuthnDelete" @click="auth.DeleteWebAuthnCredential(selectedCredential.ID)" :disabled="auth.isFetching" data-bs-dismiss="modal">{{ $t('settings.webauthn.modal-delete.button-delete') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -3,15 +3,20 @@ import {userStore} from "@/stores/users";
|
|||||||
import {ref,onMounted} from "vue";
|
import {ref,onMounted} from "vue";
|
||||||
import UserEditModal from "../components/UserEditModal.vue";
|
import UserEditModal from "../components/UserEditModal.vue";
|
||||||
import UserViewModal from "../components/UserViewModal.vue";
|
import UserViewModal from "../components/UserViewModal.vue";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
|
||||||
import {settingsStore} from "@/stores/settings";
|
|
||||||
|
|
||||||
const settings = settingsStore()
|
|
||||||
const users = userStore()
|
const users = userStore()
|
||||||
|
|
||||||
const editUserId = ref("")
|
const editUserId = ref("")
|
||||||
const viewedUserId = ref("")
|
const viewedUserId = ref("")
|
||||||
|
|
||||||
|
const selectAll = ref(false)
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
users.FilteredAndPaged.forEach(user => {
|
||||||
|
user.IsSelected = selectAll.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
users.LoadUsers()
|
users.LoadUsers()
|
||||||
})
|
})
|
||||||
@ -49,7 +54,7 @@ onMounted(() => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<input id="flexCheckDefault" class="form-check-input" :title="$t('general.select-all')" type="checkbox" value="">
|
<input class="form-check-input" :title="$t('general.select-all')" type="checkbox" v-model="selectAll" @change="toggleSelectAll">
|
||||||
</th><!-- select -->
|
</th><!-- select -->
|
||||||
<th scope="col"></th><!-- status -->
|
<th scope="col"></th><!-- status -->
|
||||||
<th scope="col">{{ $t('users.table-heading.id') }}</th>
|
<th scope="col">{{ $t('users.table-heading.id') }}</th>
|
||||||
@ -65,7 +70,7 @@ onMounted(() => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="user in users.FilteredAndPaged" :key="user.Identifier">
|
<tr v-for="user in users.FilteredAndPaged" :key="user.Identifier">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<input id="flexCheckDefault" class="form-check-input" type="checkbox" value="">
|
<input class="form-check-input" type="checkbox" v-model="user.IsSelected">
|
||||||
</th>
|
</th>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<span v-if="user.Disabled" class="text-danger" :title="$t('users.user-disabled') + ' ' + user.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
<span v-if="user.Disabled" class="text-danger" :title="$t('users.user-disabled') + ' ' + user.DisabledReason"><i class="fa fa-circle-xmark"></i></span>
|
||||||
@ -111,7 +116,7 @@ onMounted(() => {
|
|||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<select v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
|
113
go.mod
113
go.mod
@ -1,36 +1,35 @@
|
|||||||
module github.com/h44z/wg-portal
|
module github.com/h44z/wg-portal
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a8m/envsubst v1.4.2
|
github.com/a8m/envsubst v1.4.3
|
||||||
github.com/coreos/go-oidc/v3 v3.12.0
|
github.com/alexedwards/scs/v2 v2.8.0
|
||||||
github.com/gin-contrib/cors v1.7.3
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/gin-contrib/sessions v1.0.2
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0
|
||||||
|
github.com/go-webauthn/webauthn v0.13.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/prometheus-community/pro-bing v0.6.1
|
github.com/prometheus-community/pro-bing v0.7.0
|
||||||
github.com/prometheus/client_golang v1.20.5
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca
|
|
||||||
github.com/vardius/message-bus v1.1.5
|
github.com/vardius/message-bus v1.1.5
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
golang.org/x/crypto v0.32.0
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/oauth2 v0.25.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
|
golang.org/x/sys v0.33.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlserver v1.5.4
|
gorm.io/driver/sqlserver v1.6.0
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -38,45 +37,36 @@ require (
|
|||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.12.7 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.2.2 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dchest/uniuri v1.2.0 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
github.com/go-sql-driver/mysql v1.9.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
|
||||||
github.com/go-test/deep v1.1.1 // indirect
|
github.com/go-test/deep v1.1.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/go-webauthn/x v0.1.21 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/google/go-tpm v0.9.5 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.2 // indirect
|
github.com/jackc/pgx/v5 v5.7.4 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@ -84,35 +74,28 @@ require (
|
|||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
github.com/microsoft/go-mssqldb v1.8.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.61.0 // indirect
|
github.com/prometheus/common v0.63.0 // indirect
|
||||||
github.com/prometheus/procfs v0.15.1 // indirect
|
github.com/prometheus/procfs v0.16.0 // indirect
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/net v0.40.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.29.0 // indirect
|
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/protobuf v1.36.2 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
modernc.org/libc v1.63.0 // indirect
|
||||||
modernc.org/libc v1.61.7 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.8.1 // indirect
|
modernc.org/memory v1.10.0 // indirect
|
||||||
modernc.org/sqlite v1.34.4 // indirect
|
modernc.org/sqlite v1.37.0 // indirect
|
||||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
373
go.sum
373
go.sum
@ -1,16 +1,13 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
|
||||||
@ -19,124 +16,94 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occ
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
|
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
|
||||||
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
|
github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
||||||
|
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
|
||||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
|
||||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
|
||||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.2 h1:jxAJuN9fOot/cyz5Q6dUuMJF5OqQ6+5GfA8FjjQ0R4o=
|
|
||||||
github.com/bytedance/sonic/loader v0.2.2/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
|
|
||||||
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
|
||||||
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
|
|
||||||
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
|
|
||||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
|
|
||||||
github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM=
|
|
||||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
|
||||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
|
||||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
|
||||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
|
||||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1 h1:iw1yW3lXuurZZOv/DF9fY8Mkpvy6J9UjBiP1oDIQE/s=
|
||||||
|
github.com/go-pkgz/routegroup v1.4.1/go.mod h1:kDDPDRLRiRY1vnENrZJw1jQAzQX7fvsbsHGRQFNQfKc=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
|
||||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
github.com/go-webauthn/webauthn v0.13.0 h1:cJIL1/1l+22UekVhipziAaSgESJxokYkowUqAIsWs0Y=
|
||||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/go-webauthn/webauthn v0.13.0/go.mod h1:Oy9o2o79dbLKRPZWWgRIOdtBGAhKnDIaBp2PFkICRHs=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/go-webauthn/x v0.1.21 h1:nFbckQxudvHEJn2uy1VEi713MeSpApoAv9eRqsb9AdQ=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/go-webauthn/x v0.1.21/go.mod h1:sEYohtg1zL4An1TXIUIQ5csdmoO+WO0R4R2pGKaHYKA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
|
||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
@ -144,18 +111,20 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||||
@ -168,21 +137,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
@ -191,7 +149,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
|||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
@ -200,266 +157,214 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/
|
|||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
|
||||||
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
|
||||||
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus-community/pro-bing v0.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
|
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
|
||||||
github.com/prometheus-community/pro-bing v0.6.1/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ=
|
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
|
||||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f h1:oqdnd6OGlOUu1InG37hWcCB3a+Jy3fwjylyVboaNMwY=
|
|
||||||
github.com/toorop/gin-logrus v0.0.0-20210225092905-2c785434f26f/go.mod h1:X3Dd1SB8Gt1V968NTzpKFjMM6O8ccta2NPC6MprOxZQ=
|
|
||||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 h1:flbMkdl6HxQkLs6DDhH1UkcnFpNBOu70391STjMS0O4=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
|
||||||
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca h1:lpvAjPK+PcxnbcB8H7axIb4fMNwjX9bE4DzwPjGg8aE=
|
|
||||||
github.com/utrack/gin-csrf v0.0.0-20190424104817-40fb8d2c8fca/go.mod h1:XXKxNbpoLihvvT7orUZbs/iZayg1n4ip7iJakJPAwA8=
|
|
||||||
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
||||||
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjArrvnc=
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||||
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1 h1:0el6zOppx3oPiYWMUJWRYGvxWYh8MDmUU0j3rSWGWlI=
|
||||||
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1/go.mod h1:BJScsGUIKM+eg0CCLCcVaDTaclDM1IEXtq2r8qQnDKk=
|
||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
|
||||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
|
||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
|
||||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
|
||||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
|
||||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|
||||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
|
||||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
|
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||||
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
|
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
|
||||||
modernc.org/ccgo/v4 v4.23.10 h1:DnDZT/H6TtoJvQmVf7d8W+lVqEZpIJY/+0ENFh1LIHE=
|
|
||||||
modernc.org/ccgo/v4 v4.23.10/go.mod h1:vdN4h2WR5aEoNondUx26K7G8X+nuBscYnAEWSRmN2/0=
|
|
||||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
modernc.org/gc/v2 v2.6.1 h1:+Qf6xdG8l7B27TQ8D8lw/iFMUj1RXRBOuMUWziJOsk8=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.1/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/libc v1.61.7 h1:exz8rasFniviSgh3dH7QBnQHqYh9lolA5hVYfsiwkfo=
|
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
|
||||||
modernc.org/libc v1.61.7/go.mod h1:xspSrXRNVSfWfcfqgvZDVe/Hw5kv4FVC6IRfoms5v/0=
|
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.8.1 h1:HS1HRg1jEohnuONobEq2WrLEhLyw8+J42yLFTnllm2A=
|
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
||||||
modernc.org/memory v1.8.1/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
|
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
|
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
||||||
modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
|
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||||
|
@ -4,23 +4,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/glebarez/sqlite"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlserver"
|
||||||
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"gorm.io/gorm/utils"
|
"gorm.io/gorm/utils"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
gormMySQL "gorm.io/driver/mysql"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/driver/sqlserver"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
||||||
@ -32,13 +32,15 @@ type SysStat struct {
|
|||||||
SchemaVersion uint64 `gorm:"primaryKey,column:schema_version"`
|
SchemaVersion uint64 `gorm:"primaryKey,column:schema_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GormLogger is a custom logger for Gorm, making it use logrus.
|
// GormLogger is a custom logger for Gorm, making it use slog
|
||||||
type GormLogger struct {
|
type GormLogger struct {
|
||||||
SlowThreshold time.Duration
|
SlowThreshold time.Duration
|
||||||
SourceField string
|
SourceField string
|
||||||
IgnoreErrRecordNotFound bool
|
IgnoreErrRecordNotFound bool
|
||||||
Debug bool
|
Debug bool
|
||||||
Silent bool
|
Silent bool
|
||||||
|
|
||||||
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
||||||
@ -48,6 +50,7 @@ func NewLogger(slowThreshold time.Duration, debug bool) *GormLogger {
|
|||||||
IgnoreErrRecordNotFound: true,
|
IgnoreErrRecordNotFound: true,
|
||||||
Silent: false,
|
Silent: false,
|
||||||
SourceField: "src",
|
SourceField: "src",
|
||||||
|
prefix: "GORM-SQL: ",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,25 +63,25 @@ func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
|
|||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Info(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Info(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Infof(s, args...)
|
slog.InfoContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Warn(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Warn(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Warnf(s, args...)
|
slog.WarnContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Error(ctx context.Context, s string, args ...interface{}) {
|
func (l *GormLogger) Error(ctx context.Context, s string, args ...any) {
|
||||||
if l.Silent {
|
if l.Silent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logrus.WithContext(ctx).Errorf(s, args...)
|
slog.ErrorContext(ctx, l.prefix+s, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
@ -88,36 +91,40 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (stri
|
|||||||
|
|
||||||
elapsed := time.Since(begin)
|
elapsed := time.Since(begin)
|
||||||
sql, rows := fc()
|
sql, rows := fc()
|
||||||
fields := logrus.Fields{
|
|
||||||
"rows": rows,
|
attrs := []any{
|
||||||
"duration": elapsed,
|
"rows", rows,
|
||||||
|
"duration", elapsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.SourceField != "" {
|
if l.SourceField != "" {
|
||||||
fields[l.SourceField] = utils.FileWithLineNum()
|
attrs = append(attrs, l.SourceField, utils.FileWithLineNum())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.IgnoreErrRecordNotFound) {
|
if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.IgnoreErrRecordNotFound) {
|
||||||
fields[logrus.ErrorKey] = err
|
attrs = append(attrs, "error", err)
|
||||||
logrus.WithContext(ctx).WithFields(fields).Errorf("%s", sql)
|
slog.ErrorContext(ctx, l.prefix+sql, attrs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
|
if l.SlowThreshold != 0 && elapsed > l.SlowThreshold {
|
||||||
logrus.WithContext(ctx).WithFields(fields).Warnf("%s", sql)
|
slog.WarnContext(ctx, l.prefix+sql, attrs...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.Debug {
|
if l.Debug {
|
||||||
logrus.WithContext(ctx).WithFields(fields).Tracef("%s", sql)
|
slog.DebugContext(ctx, l.prefix+sql, attrs...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new database connection and returns a Gorm database instance.
|
||||||
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
var gormDb *gorm.DB
|
var gormDb *gorm.DB
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch cfg.Type {
|
switch cfg.Type {
|
||||||
case config.DatabaseMySQL:
|
case config.DatabaseMySQL:
|
||||||
gormDb, err = gorm.Open(gormMySQL.Open(cfg.DSN), &gorm.Config{
|
gormDb, err = gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{
|
||||||
Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
|
Logger: NewLogger(cfg.SlowQueryThreshold, cfg.Debug),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -172,6 +179,7 @@ type SqlRepo struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSqlRepository creates a new SqlRepo instance.
|
||||||
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
||||||
repo := &SqlRepo{
|
repo := &SqlRepo{
|
||||||
db: db,
|
db: db,
|
||||||
@ -210,13 +218,15 @@ func (r *SqlRepo) preCheck() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) migrate() error {
|
func (r *SqlRepo) migrate() error {
|
||||||
logrus.Tracef("sysstat migration: %v", r.db.AutoMigrate(&SysStat{}))
|
slog.Debug("running migration: sys-stat", "result", r.db.AutoMigrate(&SysStat{}))
|
||||||
logrus.Tracef("user migration: %v", r.db.AutoMigrate(&domain.User{}))
|
slog.Debug("running migration: user", "result", r.db.AutoMigrate(&domain.User{}))
|
||||||
logrus.Tracef("interface migration: %v", r.db.AutoMigrate(&domain.Interface{}))
|
slog.Debug("running migration: user webauthn credentials", "result",
|
||||||
logrus.Tracef("peer migration: %v", r.db.AutoMigrate(&domain.Peer{}))
|
r.db.AutoMigrate(&domain.UserWebauthnCredential{}))
|
||||||
logrus.Tracef("peer status migration: %v", r.db.AutoMigrate(&domain.PeerStatus{}))
|
slog.Debug("running migration: interface", "result", r.db.AutoMigrate(&domain.Interface{}))
|
||||||
logrus.Tracef("interface status migration: %v", r.db.AutoMigrate(&domain.InterfaceStatus{}))
|
slog.Debug("running migration: peer", "result", r.db.AutoMigrate(&domain.Peer{}))
|
||||||
logrus.Tracef("audit data migration: %v", r.db.AutoMigrate(&domain.AuditEntry{}))
|
slog.Debug("running migration: peer status", "result", r.db.AutoMigrate(&domain.PeerStatus{}))
|
||||||
|
slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{}))
|
||||||
|
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
|
||||||
|
|
||||||
existingSysStat := SysStat{}
|
existingSysStat := SysStat{}
|
||||||
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
||||||
@ -228,7 +238,7 @@ func (r *SqlRepo) migrate() error {
|
|||||||
if err := r.db.Create(&sysStat).Error; err != nil {
|
if err := r.db.Create(&sysStat).Error; err != nil {
|
||||||
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
|
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("sysstat entry for schema version %d written", SchemaVersion)
|
slog.Debug("sys-stat entry written", "schema_version", SchemaVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -236,6 +246,8 @@ func (r *SqlRepo) migrate() error {
|
|||||||
|
|
||||||
// region interfaces
|
// region interfaces
|
||||||
|
|
||||||
|
// GetInterface returns the interface with the given id.
|
||||||
|
// If no interface is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
|
||||||
var in domain.Interface
|
var in domain.Interface
|
||||||
|
|
||||||
@ -251,6 +263,8 @@ func (r *SqlRepo) GetInterface(ctx context.Context, id domain.InterfaceIdentifie
|
|||||||
return &in, nil
|
return &in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceAndPeers returns the interface with the given id and all peers associated with it.
|
||||||
|
// If no interface is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.Interface,
|
*domain.Interface,
|
||||||
[]domain.Peer,
|
[]domain.Peer,
|
||||||
@ -269,6 +283,7 @@ func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceI
|
|||||||
return in, peers, nil
|
return in, peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeersStats returns the stats for the given peer ids. The order of the returned stats is not guaranteed.
|
||||||
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -284,6 +299,7 @@ func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifie
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllInterfaces returns all interfaces.
|
||||||
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||||
var interfaces []domain.Interface
|
var interfaces []domain.Interface
|
||||||
|
|
||||||
@ -295,6 +311,8 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err
|
|||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceStats returns the stats for the given interface id.
|
||||||
|
// If no stats are found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||||
*domain.InterfaceStatus,
|
*domain.InterfaceStatus,
|
||||||
error,
|
error,
|
||||||
@ -319,6 +337,8 @@ func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIden
|
|||||||
return &stat, nil
|
return &stat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindInterfaces returns all interfaces that match the given search string.
|
||||||
|
// The search string is matched against the interface identifier and display name.
|
||||||
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) {
|
||||||
var users []domain.Interface
|
var users []domain.Interface
|
||||||
|
|
||||||
@ -335,6 +355,7 @@ func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.I
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveInterface updates the interface with the given id.
|
||||||
func (r *SqlRepo) SaveInterface(
|
func (r *SqlRepo) SaveInterface(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@ -410,6 +431,7 @@ func (r *SqlRepo) upsertInterface(ui *domain.ContextUserInfo, tx *gorm.DB, in *d
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteInterface deletes the interface with the given id.
|
||||||
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
|
err := tx.Where("interface_identifier = ?", id).Delete(&domain.Peer{}).Error
|
||||||
@ -436,6 +458,7 @@ func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdenti
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaceIps returns a map of interface identifiers to their respective IP addresses.
|
||||||
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
||||||
var ips []struct {
|
var ips []struct {
|
||||||
domain.Cidr
|
domain.Cidr
|
||||||
@ -461,6 +484,8 @@ func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIden
|
|||||||
|
|
||||||
// region peers
|
// region peers
|
||||||
|
|
||||||
|
// GetPeer returns the peer with the given id.
|
||||||
|
// If no peer is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
|
||||||
var peer domain.Peer
|
var peer domain.Peer
|
||||||
|
|
||||||
@ -476,6 +501,7 @@ func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domai
|
|||||||
return &peer, nil
|
return &peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfacePeers returns all peers associated with the given interface id.
|
||||||
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@ -487,6 +513,8 @@ func (r *SqlRepo) GetInterfacePeers(ctx context.Context, id domain.InterfaceIden
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindInterfacePeers returns all peers associated with the given interface id that match the given search string.
|
||||||
|
// The search string is matched against the peer identifier, display name and IP address.
|
||||||
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
|
func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIdentifier, search string) (
|
||||||
[]domain.Peer,
|
[]domain.Peer,
|
||||||
error,
|
error,
|
||||||
@ -506,6 +534,7 @@ func (r *SqlRepo) FindInterfacePeers(ctx context.Context, id domain.InterfaceIde
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserPeers returns all peers associated with the given user id.
|
||||||
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@ -517,6 +546,8 @@ func (r *SqlRepo) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindUserPeers returns all peers associated with the given user id that match the given search string.
|
||||||
|
// The search string is matched against the peer identifier, display name and IP address.
|
||||||
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
|
func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, search string) ([]domain.Peer, error) {
|
||||||
var peers []domain.Peer
|
var peers []domain.Peer
|
||||||
|
|
||||||
@ -533,6 +564,8 @@ func (r *SqlRepo) FindUserPeers(ctx context.Context, id domain.UserIdentifier, s
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePeer updates the peer with the given id.
|
||||||
|
// If no existing peer is found, a new peer is created.
|
||||||
func (r *SqlRepo) SavePeer(
|
func (r *SqlRepo) SavePeer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@ -607,6 +640,7 @@ func (r *SqlRepo) upsertPeer(ui *domain.ContextUserInfo, tx *gorm.DB, peer *doma
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeer deletes the peer with the given id.
|
||||||
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
|
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
|
err := tx.Delete(&domain.PeerStatus{PeerId: id}).Error
|
||||||
@ -628,6 +662,7 @@ func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeerIps returns a map of peer identifiers to their respective IP addresses.
|
||||||
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
|
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
|
||||||
var ips []struct {
|
var ips []struct {
|
||||||
domain.Cidr
|
domain.Cidr
|
||||||
@ -649,6 +684,7 @@ func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]d
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsedIpsPerSubnet returns a map of subnets to their respective used IP addresses.
|
||||||
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
|
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (
|
||||||
map[domain.Cidr][]domain.Cidr,
|
map[domain.Cidr][]domain.Cidr,
|
||||||
error,
|
error,
|
||||||
@ -707,10 +743,12 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
|
|||||||
|
|
||||||
// region users
|
// region users
|
||||||
|
|
||||||
|
// GetUser returns the user with the given id.
|
||||||
|
// If no user is found, an error domain.ErrNotFound is returned.
|
||||||
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||||
var user domain.User
|
var user domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).First(&user, id).Error
|
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").First(&user, id).Error
|
||||||
|
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, domain.ErrNotFound
|
return nil, domain.ErrNotFound
|
||||||
@ -722,10 +760,13 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail returns the user with the given email.
|
||||||
|
// If no user is found, an error domain.ErrNotFound is returned.
|
||||||
|
// If multiple users are found, an error domain.ErrNotUnique is returned.
|
||||||
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
|
err := r.db.WithContext(ctx).Where("email = ?", email).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, domain.ErrNotFound
|
return nil, domain.ErrNotFound
|
||||||
}
|
}
|
||||||
@ -746,10 +787,26 @@ func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.Use
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByWebAuthnCredential returns the user with the given webauthn credential id.
|
||||||
|
func (r *SqlRepo) GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error) {
|
||||||
|
var credential domain.UserWebauthnCredential
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("credential_identifier = ?", credentialIdBase64).First(&credential).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, domain.ErrNotFound
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetUser(ctx, domain.UserIdentifier(credential.UserIdentifier))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUsers returns all users.
|
||||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
err := r.db.WithContext(ctx).Find(&users).Error
|
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -757,6 +814,8 @@ func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindUsers returns all users that match the given search string.
|
||||||
|
// The search string is matched against the user identifier, firstname, lastname and email.
|
||||||
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
|
func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User, error) {
|
||||||
var users []domain.User
|
var users []domain.User
|
||||||
|
|
||||||
@ -766,6 +825,7 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
|
|||||||
Or("firstname LIKE ?", searchValue).
|
Or("firstname LIKE ?", searchValue).
|
||||||
Or("lastname LIKE ?", searchValue).
|
Or("lastname LIKE ?", searchValue).
|
||||||
Or("email LIKE ?", searchValue).
|
Or("email LIKE ?", searchValue).
|
||||||
|
Preload("WebAuthnCredentialList").
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -774,6 +834,8 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveUser updates the user with the given id.
|
||||||
|
// If no user is found, a new user is created.
|
||||||
func (r *SqlRepo) SaveUser(
|
func (r *SqlRepo) SaveUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.UserIdentifier,
|
id domain.UserIdentifier,
|
||||||
@ -807,8 +869,9 @@ func (r *SqlRepo) SaveUser(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes the user with the given id.
|
||||||
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
func (r *SqlRepo) DeleteUser(ctx context.Context, id domain.UserIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Delete(&domain.User{}, id).Error
|
err := r.db.WithContext(ctx).Unscoped().Select(clause.Associations).Delete(&domain.User{Identifier: id}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -852,6 +915,11 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = tx.Session(&gorm.Session{FullSaveAssociations: true}).Unscoped().Model(user).Association("WebAuthnCredentialList").Unscoped().Replace(user.WebAuthnCredentialList)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update users webauthn credentials: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -859,6 +927,8 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
|
|||||||
|
|
||||||
// region statistics
|
// region statistics
|
||||||
|
|
||||||
|
// UpdateInterfaceStatus updates the interface status with the given id.
|
||||||
|
// If no interface status is found, a new one is created.
|
||||||
func (r *SqlRepo) UpdateInterfaceStatus(
|
func (r *SqlRepo) UpdateInterfaceStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@ -919,6 +989,8 @@ func (r *SqlRepo) upsertInterfaceStatus(tx *gorm.DB, in *domain.InterfaceStatus)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdatePeerStatus updates the peer status with the given id.
|
||||||
|
// If no peer status is found, a new one is created.
|
||||||
func (r *SqlRepo) UpdatePeerStatus(
|
func (r *SqlRepo) UpdatePeerStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id domain.PeerIdentifier,
|
id domain.PeerIdentifier,
|
||||||
@ -976,6 +1048,7 @@ func (r *SqlRepo) upsertPeerStatus(tx *gorm.DB, in *domain.PeerStatus) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeerStatus deletes the peer status with the given id.
|
||||||
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
|
func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier) error {
|
||||||
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
|
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{}, id).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -989,6 +1062,7 @@ func (r *SqlRepo) DeletePeerStatus(ctx context.Context, id domain.PeerIdentifier
|
|||||||
|
|
||||||
// region audit
|
// region audit
|
||||||
|
|
||||||
|
// SaveAuditEntry saves the given audit entry.
|
||||||
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
|
func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry) error {
|
||||||
err := r.db.WithContext(ctx).Save(entry).Error
|
err := r.db.WithContext(ctx).Save(entry).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -998,4 +1072,16 @@ func (r *SqlRepo) SaveAuditEntry(ctx context.Context, entry *domain.AuditEntry)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllAuditEntries retrieves all audit entries from the database.
|
||||||
|
// The entries are ordered by timestamp, with the newest entries first.
|
||||||
|
func (r *SqlRepo) GetAllAuditEntries(ctx context.Context) ([]domain.AuditEntry, error) {
|
||||||
|
var entries []domain.AuditEntry
|
||||||
|
err := r.db.WithContext(ctx).Order("created_at desc").Find(&entries).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
// endregion audit
|
// endregion audit
|
||||||
|
@ -5,17 +5,14 @@ package adapters
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func tempSqliteDb(t *testing.T) *gorm.DB {
|
func tempSqliteDb(t *testing.T) *gorm.DB {
|
||||||
|
|
||||||
// github.com/mattn/go-sqlite3
|
|
||||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -2,8 +2,8 @@ package adapters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
@ -12,6 +12,7 @@ type FilesystemRepo struct {
|
|||||||
basePath string
|
basePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFileSystemRepository creates a new FilesystemRepo instance.
|
||||||
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
||||||
if basePath == "" {
|
if basePath == "" {
|
||||||
return nil, nil // no path, return empty repository
|
return nil, nil // no path, return empty repository
|
||||||
@ -26,6 +27,10 @@ func NewFileSystemRepository(basePath string) (*FilesystemRepo, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteFile writes the given contents to the given path.
|
||||||
|
// The path is relative to the base path of the repository.
|
||||||
|
// If the parent directory does not exist, it is created.
|
||||||
|
// If the file already exists, it is overwritten.
|
||||||
func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
||||||
filePath := filepath.Join(r.basePath, path)
|
filePath := filepath.Join(r.basePath, path)
|
||||||
parentDirectory := filepath.Dir(filePath)
|
parentDirectory := filepath.Dir(filePath)
|
||||||
@ -36,11 +41,11 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
|||||||
|
|
||||||
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to open file %s: %w", file.Name(), err)
|
return fmt.Errorf("failed to open file %s: %w", filePath, err)
|
||||||
}
|
}
|
||||||
defer func(file *os.File) {
|
defer func(file *os.File) {
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
logrus.Errorf("failed to close file %s: %v", file.Name(), err)
|
slog.Error("failed to close file", "file", file.Name(), "error", err)
|
||||||
}
|
}
|
||||||
}(file)
|
}(file)
|
||||||
|
|
||||||
@ -50,5 +55,17 @@ func (r *FilesystemRepo) WriteFile(path string, contents io.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFile deletes the file at the given path.
|
||||||
|
// The path is relative to the base path of the repository.
|
||||||
|
// If the file does not exist, it is ignored.
|
||||||
|
func (r *FilesystemRepo) DeleteFile(path string) error {
|
||||||
|
filePath := filepath.Join(r.basePath, path)
|
||||||
|
|
||||||
|
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to delete file %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -5,23 +5,26 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mail "github.com/xhit/go-simple-mail/v2"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
mail "github.com/xhit/go-simple-mail/v2"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MailRepo struct {
|
type MailRepo struct {
|
||||||
cfg *config.MailConfig
|
cfg *config.MailConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSmtpMailRepo creates a new MailRepo instance.
|
||||||
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
|
func NewSmtpMailRepo(cfg config.MailConfig) MailRepo {
|
||||||
return MailRepo{cfg: &cfg}
|
return MailRepo{cfg: &cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends a mail.
|
// Send sends a mail using SMTP.
|
||||||
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
|
func (r MailRepo) Send(_ context.Context, subject, body string, to []string, options *domain.MailOptions) error {
|
||||||
if options == nil {
|
if options == nil {
|
||||||
options = &domain.MailOptions{}
|
options = &domain.MailOptions{}
|
||||||
|
@ -2,16 +2,18 @@ package adapters
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetricsServer struct {
|
type MetricsServer struct {
|
||||||
@ -84,16 +86,16 @@ func NewMetricsServer(cfg *config.Config) *MetricsServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run starts the metrics server
|
// Run starts the metrics server. The function blocks until the context is cancelled.
|
||||||
func (m *MetricsServer) Run(ctx context.Context) {
|
func (m *MetricsServer) Run(ctx context.Context) {
|
||||||
// Run the metrics server in a goroutine
|
// Run the metrics server in a goroutine
|
||||||
go func() {
|
go func() {
|
||||||
if err := m.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := m.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
logrus.Errorf("metrics service on %s exited: %v", m.Addr, err)
|
slog.Error("metrics service exited", "address", m.Addr, "error", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
logrus.Infof("started metrics service on %s", m.Addr)
|
slog.Info("started metrics service", "address", m.Addr)
|
||||||
|
|
||||||
// Wait for the context to be done
|
// Wait for the context to be done
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
@ -102,11 +104,11 @@ func (m *MetricsServer) Run(ctx context.Context) {
|
|||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Attempt to gracefully shutdown the metrics server
|
// Attempt to gracefully shut down the metrics server
|
||||||
if err := m.Shutdown(shutdownCtx); err != nil {
|
if err := m.Shutdown(shutdownCtx); err != nil {
|
||||||
logrus.Errorf("metrics service on %s shutdown failed: %v", m.Addr, err)
|
slog.Error("metrics service shutdown failed", "address", m.Addr, "error", err)
|
||||||
} else {
|
} else {
|
||||||
logrus.Infof("metrics service on %s shutdown gracefully", m.Addr)
|
slog.Info("metrics service shutdown gracefully", "address", m.Addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,9 +123,9 @@ func (m *MetricsServer) UpdateInterfaceMetrics(status domain.InterfaceStatus) {
|
|||||||
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
|
func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerStatus) {
|
||||||
labels := []string{
|
labels := []string{
|
||||||
string(peer.InterfaceIdentifier),
|
string(peer.InterfaceIdentifier),
|
||||||
string(peer.Interface.AddressStr()),
|
peer.Interface.AddressStr(),
|
||||||
string(status.PeerId),
|
string(status.PeerId),
|
||||||
string(peer.DisplayName),
|
peer.DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.LastHandshake != nil {
|
if status.LastHandshake != nil {
|
||||||
@ -131,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS
|
|||||||
}
|
}
|
||||||
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived))
|
||||||
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted))
|
||||||
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected()))
|
m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected))
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@ package adapters
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal"
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
|
// WgQuickRepo implements higher level wg-quick like interactions like setting DNS, routing tables or interface hooks.
|
||||||
@ -17,6 +17,7 @@ type WgQuickRepo struct {
|
|||||||
resolvConfIfacePrefix string
|
resolvConfIfacePrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWgQuickRepo creates a new WgQuickRepo instance.
|
||||||
func NewWgQuickRepo() *WgQuickRepo {
|
func NewWgQuickRepo() *WgQuickRepo {
|
||||||
return &WgQuickRepo{
|
return &WgQuickRepo{
|
||||||
shellCmd: "bash",
|
shellCmd: "bash",
|
||||||
@ -24,12 +25,16 @@ func NewWgQuickRepo() *WgQuickRepo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteInterfaceHook executes the given hook command.
|
||||||
|
// The hook command can contain the following placeholders:
|
||||||
|
//
|
||||||
|
// %i: the interface identifier.
|
||||||
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
|
||||||
if hookCmd == "" {
|
if hookCmd == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Tracef("interface %s: executing hook %s", id, hookCmd)
|
slog.Debug("executing interface hook", "interface", id, "hook", hookCmd)
|
||||||
err := r.exec(hookCmd, id)
|
err := r.exec(hookCmd, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to exec hook: %w", err)
|
return fmt.Errorf("failed to exec hook: %w", err)
|
||||||
@ -38,6 +43,7 @@ func (r *WgQuickRepo) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCm
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDNS sets the DNS settings for the given interface. It uses resolvconf to set the DNS settings.
|
||||||
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
|
func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
|
||||||
if dnsStr == "" && dnsSearchStr == "" {
|
if dnsStr == "" && dnsSearchStr == "" {
|
||||||
return nil
|
return nil
|
||||||
@ -67,6 +73,7 @@ func (r *WgQuickRepo) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnsetDNS unsets the DNS settings for the given interface. It uses resolvconf to unset the DNS settings.
|
||||||
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
|
func (r *WgQuickRepo) UnsetDNS(id domain.InterfaceIdentifier) error {
|
||||||
dnsCommand := "resolvconf -d %resPref%i -f"
|
dnsCommand := "resolvconf -d %resPref%i -f"
|
||||||
|
|
||||||
@ -99,6 +106,8 @@ func (r *WgQuickRepo) exec(command string, interfaceId domain.InterfaceIdentifie
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
||||||
}
|
}
|
||||||
logrus.Tracef("executed shell command %s, with output: %s", commandWithInterfaceName, string(out))
|
slog.Debug("executed shell command",
|
||||||
|
"command", commandWithInterfaceName,
|
||||||
|
"output", string(out))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WgRepo implements all low-level WireGuard interactions.
|
// WgRepo implements all low-level WireGuard interactions.
|
||||||
@ -19,6 +20,8 @@ type WgRepo struct {
|
|||||||
nl lowlevel.NetlinkClient
|
nl lowlevel.NetlinkClient
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWireGuardRepository creates a new WgRepo instance.
|
||||||
|
// This repository is used to interact with the WireGuard kernel or userspace module.
|
||||||
func NewWireGuardRepository() *WgRepo {
|
func NewWireGuardRepository() *WgRepo {
|
||||||
wg, err := wgctrl.New()
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,6 +38,7 @@ func NewWireGuardRepository() *WgRepo {
|
|||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterfaces returns all existing WireGuard interfaces.
|
||||||
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, error) {
|
||||||
devices, err := r.wg.Devices()
|
devices, err := r.wg.Devices()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -53,10 +57,14 @@ func (r *WgRepo) GetInterfaces(_ context.Context) ([]domain.PhysicalInterface, e
|
|||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetInterface returns the interface with the given id.
|
||||||
|
// If no interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
func (r *WgRepo) GetInterface(_ context.Context, id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
return r.getInterface(id)
|
return r.getInterface(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeers returns all peers associated with the given interface id.
|
||||||
|
// If the requested interface is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier) ([]domain.PhysicalPeer, error) {
|
||||||
device, err := r.wg.Device(string(deviceId))
|
device, err := r.wg.Device(string(deviceId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -75,6 +83,8 @@ func (r *WgRepo) GetPeers(_ context.Context, deviceId domain.InterfaceIdentifier
|
|||||||
return peers, nil
|
return peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeer returns the peer with the given id.
|
||||||
|
// If the requested interface or peer is found, an error os.ErrNotExist is returned.
|
||||||
func (r *WgRepo) GetPeer(
|
func (r *WgRepo) GetPeer(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
@ -156,6 +166,9 @@ func (r *WgRepo) convertWireGuardPeer(peer *wgtypes.Peer) (domain.PhysicalPeer,
|
|||||||
return peerModel, nil
|
return peerModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveInterface updates the interface with the given id.
|
||||||
|
// If no existing interface is found, a new interface is created.
|
||||||
|
// Updating the interface does not interrupt any existing connections.
|
||||||
func (r *WgRepo) SaveInterface(
|
func (r *WgRepo) SaveInterface(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
id domain.InterfaceIdentifier,
|
id domain.InterfaceIdentifier,
|
||||||
@ -186,10 +199,10 @@ func (r *WgRepo) SaveInterface(
|
|||||||
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
func (r *WgRepo) getOrCreateInterface(id domain.InterfaceIdentifier) (*domain.PhysicalInterface, error) {
|
||||||
device, err := r.getInterface(id)
|
device, err := r.getInterface(id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return device, nil
|
return device, nil // interface exists
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("device error: %w", err)
|
return nil, fmt.Errorf("device error: %w", err) // unknown error
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new device
|
// create new device
|
||||||
@ -307,6 +320,8 @@ func (r *WgRepo) updateWireGuardInterface(pi *domain.PhysicalInterface) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteInterface deletes the interface with the given id.
|
||||||
|
// If the requested interface is found, no error is returned.
|
||||||
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
func (r *WgRepo) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
|
||||||
if err := r.deleteLowLevelInterface(id); err != nil {
|
if err := r.deleteLowLevelInterface(id); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -333,6 +348,8 @@ func (r *WgRepo) deleteLowLevelInterface(id domain.InterfaceIdentifier) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavePeer updates the peer with the given id.
|
||||||
|
// If no existing peer is found, a new peer is created.
|
||||||
func (r *WgRepo) SavePeer(
|
func (r *WgRepo) SavePeer(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
deviceId domain.InterfaceIdentifier,
|
deviceId domain.InterfaceIdentifier,
|
||||||
@ -362,10 +379,10 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
|
|||||||
) {
|
) {
|
||||||
peer, err := r.getPeer(deviceId, id)
|
peer, err := r.getPeer(deviceId, id)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return peer, nil
|
return peer, nil // peer exists
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, fmt.Errorf("peer error: %w", err)
|
return nil, fmt.Errorf("peer error: %w", err) // unknown error
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new peer
|
// create new peer
|
||||||
@ -376,8 +393,14 @@ func (r *WgRepo) getOrCreatePeer(deviceId domain.InterfaceIdentifier, id domain.
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer create error for %s: %w", id.ToPublicKey(), err)
|
||||||
|
}
|
||||||
|
|
||||||
peer, err = r.getPeer(deviceId, id)
|
peer, err = r.getPeer(deviceId, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("peer error after create: %w", err)
|
||||||
|
}
|
||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,6 +447,8 @@ func (r *WgRepo) updatePeer(deviceId domain.InterfaceIdentifier, pp *domain.Phys
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeletePeer deletes the peer with the given id.
|
||||||
|
// If the requested interface or peer is found, no error is returned.
|
||||||
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
func (r *WgRepo) DeletePeer(_ context.Context, deviceId domain.InterfaceIdentifier, id domain.PeerIdentifier) error {
|
||||||
if !id.IsPublicKey() {
|
if !id.IsPublicKey() {
|
||||||
return errors.New("invalid public key")
|
return errors.New("invalid public key")
|
||||||
|
@ -12,11 +12,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal"
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
"github.com/h44z/wg-portal/internal/domain"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// setup WireGuard manager with no linked store
|
// setup WireGuard manager with no linked store
|
||||||
@ -43,12 +43,12 @@ func Test_wgRepository_GetInterfaces(t *testing.T) {
|
|||||||
mgr := setup(t)
|
mgr := setup(t)
|
||||||
|
|
||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
|
interfaceName2 := domain.InterfaceIdentifier("wg_test_002")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName2)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName2))
|
||||||
err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
|
err = mgr.SaveInterface(context.Background(), interfaceName2, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -66,9 +66,10 @@ func TestWireGuardCreateInterface(t *testing.T) {
|
|||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
ipAddress := "10.11.12.13"
|
ipAddress := "10.11.12.13"
|
||||||
ipV6Address := "1337:d34d:b33f::2"
|
ipV6Address := "1337:d34d:b33f::2"
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
|
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
err := mgr.SaveInterface(context.Background(), interfaceName,
|
||||||
|
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||||
pi.Addresses = []domain.Cidr{
|
pi.Addresses = []domain.Cidr{
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
||||||
@ -90,7 +91,7 @@ func TestWireGuardUpdateInterface(t *testing.T) {
|
|||||||
mgr := setup(t)
|
mgr := setup(t)
|
||||||
|
|
||||||
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
interfaceName := domain.InterfaceIdentifier("wg_test_001")
|
||||||
defer mgr.DeleteInterface(context.Background(), interfaceName)
|
defer internal.LogError(mgr.DeleteInterface(context.Background(), interfaceName))
|
||||||
|
|
||||||
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
err := mgr.SaveInterface(context.Background(), interfaceName, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -102,7 +103,8 @@ func TestWireGuardUpdateInterface(t *testing.T) {
|
|||||||
|
|
||||||
ipAddress := "10.11.12.13"
|
ipAddress := "10.11.12.13"
|
||||||
ipV6Address := "1337:d34d:b33f::2"
|
ipV6Address := "1337:d34d:b33f::2"
|
||||||
err = mgr.SaveInterface(context.Background(), interfaceName, func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
err = mgr.SaveInterface(context.Background(), interfaceName,
|
||||||
|
func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error) {
|
||||||
pi.Addresses = []domain.Cidr{
|
pi.Addresses = []domain.Cidr{
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipAddress), Mask: net.CIDRMask(24, 32)}),
|
||||||
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
domain.CidrFromIpNet(net.IPNet{IP: net.ParseIP(ipV6Address), Mask: net.CIDRMask(64, 128)}),
|
||||||
|
@ -11,6 +11,29 @@
|
|||||||
},
|
},
|
||||||
"basePath": "/api/v0",
|
"basePath": "/api/v0",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/audit/entries": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Audit"
|
||||||
|
],
|
||||||
|
"summary": "Get all available audit entries. Ordered by timestamp.",
|
||||||
|
"operationId": "audit_handleEntriesGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.AuditEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/login": {
|
"/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -34,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/logout": {
|
"/auth/login/{provider}/callback": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@ -42,8 +65,8 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Get all available external login providers.",
|
"summary": "Handle the OAuth callback.",
|
||||||
"operationId": "auth_handleLogoutGet",
|
"operationId": "auth_handleOauthCallbackGet",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -57,6 +80,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/login/{provider}/init": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Initiate the OAuth login flow.",
|
||||||
|
"operationId": "auth_handleOauthInitiateGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/logout": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Get all available external login providers.",
|
||||||
|
"operationId": "auth_handleLogoutPost",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/providers": {
|
"/auth/providers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -109,30 +175,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/{provider}/callback": {
|
"/auth/webauthn/credential/{id}": {
|
||||||
"get": {
|
"put": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Handle the OAuth callback.",
|
"summary": "Update a WebAuthn credential.",
|
||||||
"operationId": "auth_handleOauthCallbackGet",
|
"operationId": "auth_handleWebAuthnCredentialsPut",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64 encoded Credential ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Credential name",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Delete a WebAuthn credential.",
|
||||||
|
"operationId": "auth_handleWebAuthnCredentialsDelete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64 encoded Credential ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/auth/{provider}/init": {
|
"/auth/webauthn/credentials": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@ -140,15 +254,67 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Initiate the OAuth login flow.",
|
"summary": "Get all available external login providers.",
|
||||||
"operationId": "auth_handleOauthInitiateGet",
|
"operationId": "auth_handleWebAuthnCredentialsGet",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/model.LoginProviderInfo"
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/webauthn/login/finish": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Finish the WebAuthn login process.",
|
||||||
|
"operationId": "auth_handleWebAuthnLoginFinish",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/webauthn/register/finish": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Finish the WebAuthn registration process.",
|
||||||
|
"operationId": "auth_handleWebAuthnRegisterFinish",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"default": "\"\"",
|
||||||
|
"description": "Credential name",
|
||||||
|
"name": "credential_name",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.WebAuthnCredentialResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,6 +337,9 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1363,6 +1532,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/user/{id}/interfaces": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get interfaces for the given user. Returns an empty list if self provisioning is disabled.",
|
||||||
|
"operationId": "users_handleInterfacesGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user/{id}/peers": {
|
"/user/{id}/peers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@ -1373,6 +1586,15 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peers for the given user.",
|
"summary": "Get peers for the given user.",
|
||||||
"operationId": "users_handlePeersGet",
|
"operationId": "users_handlePeersGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -1408,6 +1630,15 @@
|
|||||||
],
|
],
|
||||||
"summary": "Get peer stats for the given user.",
|
"summary": "Get peer stats for the given user.",
|
||||||
"operationId": "users_handleStatsGet",
|
"operationId": "users_handleStatsGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The user identifier",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
@ -1432,6 +1663,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"model.AuditEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ContextUser": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"Message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Origin": {
|
||||||
|
"description": "origin: for example user auth, stats, ...",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Severity": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.ConfigOption-array_string": {
|
"model.ConfigOption-array_string": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -1537,6 +1792,10 @@
|
|||||||
"EnabledPeers": {
|
"EnabledPeers": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "the filename of the config file, for example: wg0.conf",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -1778,6 +2037,10 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "the filename of the config file, for example: wg_peer_x.conf",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "a firewall mark",
|
"description": "a firewall mark",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
@ -1968,14 +2231,23 @@
|
|||||||
"ApiAdminOnly": {
|
"ApiAdminOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"LoginFormVisible": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"MailLinkOnly": {
|
"MailLinkOnly": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"MinPasswordLength": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"PersistentConfigSupported": {
|
"PersistentConfigSupported": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"SelfProvisioning": {
|
"SelfProvisioning": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"WebAuthnEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2044,6 +2316,28 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"model.WebAuthnCredentialRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model.WebAuthnCredentialResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"CreatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ID": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"Name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,21 @@
|
|||||||
basePath: /api/v0
|
basePath: /api/v0
|
||||||
definitions:
|
definitions:
|
||||||
|
model.AuditEntry:
|
||||||
|
properties:
|
||||||
|
ContextUser:
|
||||||
|
type: string
|
||||||
|
Id:
|
||||||
|
type: integer
|
||||||
|
Message:
|
||||||
|
type: string
|
||||||
|
Origin:
|
||||||
|
description: 'origin: for example user auth, stats, ...'
|
||||||
|
type: string
|
||||||
|
Severity:
|
||||||
|
type: string
|
||||||
|
Timestamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
model.ConfigOption-array_string:
|
model.ConfigOption-array_string:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
Overridable:
|
||||||
@ -72,6 +88,9 @@ definitions:
|
|||||||
type: array
|
type: array
|
||||||
EnabledPeers:
|
EnabledPeers:
|
||||||
type: integer
|
type: integer
|
||||||
|
Filename:
|
||||||
|
description: 'the filename of the config file, for example: wg0.conf'
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
description: a firewall mark
|
description: a firewall mark
|
||||||
type: integer
|
type: integer
|
||||||
@ -240,6 +259,9 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Filename:
|
||||||
|
description: 'the filename of the config file, for example: wg_peer_x.conf'
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.ConfigOption-uint32'
|
- $ref: '#/definitions/model.ConfigOption-uint32'
|
||||||
@ -359,12 +381,18 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
ApiAdminOnly:
|
ApiAdminOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
LoginFormVisible:
|
||||||
|
type: boolean
|
||||||
MailLinkOnly:
|
MailLinkOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
MinPasswordLength:
|
||||||
|
type: integer
|
||||||
PersistentConfigSupported:
|
PersistentConfigSupported:
|
||||||
type: boolean
|
type: boolean
|
||||||
SelfProvisioning:
|
SelfProvisioning:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
WebAuthnEnabled:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
model.User:
|
model.User:
|
||||||
properties:
|
properties:
|
||||||
@ -411,6 +439,20 @@ definitions:
|
|||||||
Source:
|
Source:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.WebAuthnCredentialRequest:
|
||||||
|
properties:
|
||||||
|
Name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
model.WebAuthnCredentialResponse:
|
||||||
|
properties:
|
||||||
|
CreatedAt:
|
||||||
|
type: string
|
||||||
|
ID:
|
||||||
|
type: string
|
||||||
|
Name:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
info:
|
info:
|
||||||
contact:
|
contact:
|
||||||
name: WireGuard Portal Developers
|
name: WireGuard Portal Developers
|
||||||
@ -419,9 +461,9 @@ info:
|
|||||||
title: WireGuard Portal SPA-UI API
|
title: WireGuard Portal SPA-UI API
|
||||||
version: "0.0"
|
version: "0.0"
|
||||||
paths:
|
paths:
|
||||||
/auth/{provider}/callback:
|
/audit/entries:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthCallbackGet
|
operationId: audit_handleEntriesGet
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@ -429,26 +471,11 @@ paths:
|
|||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
$ref: '#/definitions/model.AuditEntry'
|
||||||
type: array
|
type: array
|
||||||
summary: Handle the OAuth callback.
|
summary: Get all available audit entries. Ordered by timestamp.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Audit
|
||||||
/auth/{provider}/init:
|
|
||||||
get:
|
|
||||||
operationId: auth_handleOauthInitiateGet
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
|
||||||
type: array
|
|
||||||
summary: Initiate the OAuth login flow.
|
|
||||||
tags:
|
|
||||||
- Authentication
|
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
operationId: auth_handleLoginPost
|
operationId: auth_handleLoginPost
|
||||||
@ -464,9 +491,9 @@ paths:
|
|||||||
summary: Get all available external login providers.
|
summary: Get all available external login providers.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/logout:
|
/auth/login/{provider}/callback:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleLogoutGet
|
operationId: auth_handleOauthCallbackGet
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@ -476,6 +503,34 @@ paths:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
$ref: '#/definitions/model.LoginProviderInfo'
|
||||||
type: array
|
type: array
|
||||||
|
summary: Handle the OAuth callback.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/login/{provider}/init:
|
||||||
|
get:
|
||||||
|
operationId: auth_handleOauthInitiateGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.LoginProviderInfo'
|
||||||
|
type: array
|
||||||
|
summary: Initiate the OAuth login flow.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/logout:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleLogoutPost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
summary: Get all available external login providers.
|
summary: Get all available external login providers.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
@ -513,6 +568,102 @@ paths:
|
|||||||
summary: Get information about the currently logged-in user.
|
summary: Get information about the currently logged-in user.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
|
/auth/webauthn/credential/{id}:
|
||||||
|
delete:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsDelete
|
||||||
|
parameters:
|
||||||
|
- description: Base64 encoded Credential ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Delete a WebAuthn credential.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
put:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsPut
|
||||||
|
parameters:
|
||||||
|
- description: Base64 encoded Credential ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Credential name
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Update a WebAuthn credential.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/credentials:
|
||||||
|
get:
|
||||||
|
operationId: auth_handleWebAuthnCredentialsGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Get all available external login providers.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/login/finish:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleWebAuthnLoginFinish
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.User'
|
||||||
|
summary: Finish the WebAuthn login process.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/webauthn/register/finish:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleWebAuthnRegisterFinish
|
||||||
|
parameters:
|
||||||
|
- default: '""'
|
||||||
|
description: Credential name
|
||||||
|
in: query
|
||||||
|
name: credential_name
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.WebAuthnCredentialResponse'
|
||||||
|
type: array
|
||||||
|
summary: Finish the WebAuthn registration process.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
/config/frontend.js:
|
/config/frontend.js:
|
||||||
get:
|
get:
|
||||||
operationId: config_handleConfigJsGet
|
operationId: config_handleConfigJsGet
|
||||||
@ -523,6 +674,8 @@ paths:
|
|||||||
description: The JavaScript contents
|
description: The JavaScript contents
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
summary: Get the dynamic frontend configuration javascript.
|
summary: Get the dynamic frontend configuration javascript.
|
||||||
tags:
|
tags:
|
||||||
- Configuration
|
- Configuration
|
||||||
@ -1262,9 +1415,45 @@ paths:
|
|||||||
summary: Enable the REST API for the given user.
|
summary: Enable the REST API for the given user.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/{id}/interfaces:
|
||||||
|
get:
|
||||||
|
operationId: users_handleInterfacesGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Interface'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Get interfaces for the given user. Returns an empty list if self provisioning
|
||||||
|
is disabled.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/{id}/peers:
|
/user/{id}/peers:
|
||||||
get:
|
get:
|
||||||
operationId: users_handlePeersGet
|
operationId: users_handlePeersGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@ -1288,6 +1477,12 @@ paths:
|
|||||||
/user/{id}/stats:
|
/user/{id}/stats:
|
||||||
get:
|
get:
|
||||||
operationId: users_handleStatsGet
|
operationId: users_handleStatsGet
|
||||||
|
parameters:
|
||||||
|
- description: The user identifier
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"description": "This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -250,6 +251,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"description": "This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -309,6 +311,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/interface/prepare": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BasicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Interfaces"
|
||||||
|
],
|
||||||
|
"summary": "Prepare a new interface record.",
|
||||||
|
"operationId": "interfaces_handlePrepareGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Interface"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/metrics/by-interface/{id}": {
|
"/metrics/by-interface/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -547,7 +593,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Only admins can update existing records.",
|
"description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -779,7 +825,7 @@
|
|||||||
"BasicAuth": []
|
"BasicAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Only admins can create new records.",
|
"description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@ -839,6 +885,71 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/prepare/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BasicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peers"
|
||||||
|
],
|
||||||
|
"summary": "Prepare a new peer record for the given WireGuard interface.",
|
||||||
|
"operationId": "peers_handlePrepareGet",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The interface identifier.",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Peer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/provisioning/data/peer-config": {
|
"/provisioning/data/peer-config": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@ -1471,14 +1582,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models.ExpiryDate": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"time.Time": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"models.Interface": {
|
"models.Interface": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -1539,6 +1642,13 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"readOnly": true
|
"readOnly": true
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "Filename is the name of the config file for this interface.\nThis value is read only and is not settable by the user.",
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 21,
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "wg0.conf"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "FirewallMark is an optional firewall mark which is used to handle interface traffic.",
|
"description": "FirewallMark is an optional firewall mark which is used to handle interface traffic.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@ -1798,11 +1908,7 @@
|
|||||||
},
|
},
|
||||||
"ExpiresAt": {
|
"ExpiresAt": {
|
||||||
"description": "ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.",
|
"description": "ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.",
|
||||||
"allOf": [
|
"type": "string"
|
||||||
{
|
|
||||||
"$ref": "#/definitions/models.ExpiryDate"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"ExtraAllowedIPs": {
|
"ExtraAllowedIPs": {
|
||||||
"description": "ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.",
|
"description": "ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.",
|
||||||
@ -1811,6 +1917,13 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Filename": {
|
||||||
|
"description": "Filename is the name of the config file for this peer.\nThis value is read only and is not settable by the user.",
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 21,
|
||||||
|
"readOnly": true,
|
||||||
|
"example": "wg_peer_x.conf"
|
||||||
|
},
|
||||||
"FirewallMark": {
|
"FirewallMark": {
|
||||||
"description": "FirewallMark is an optional firewall mark which is used to handle peer traffic.",
|
"description": "FirewallMark is an optional firewall mark which is used to handle peer traffic.",
|
||||||
"allOf": [
|
"allOf": [
|
||||||
@ -1998,8 +2111,7 @@
|
|||||||
"models.User": {
|
"models.User": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"Identifier",
|
"Identifier"
|
||||||
"IsAdmin"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"ApiEnabled": {
|
"ApiEnabled": {
|
||||||
|
@ -42,11 +42,6 @@ definitions:
|
|||||||
description: Error message.
|
description: Error message.
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
models.ExpiryDate:
|
|
||||||
properties:
|
|
||||||
time.Time:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
models.Interface:
|
models.Interface:
|
||||||
properties:
|
properties:
|
||||||
Addresses:
|
Addresses:
|
||||||
@ -92,6 +87,14 @@ definitions:
|
|||||||
Only enabled peers are able to connect.
|
Only enabled peers are able to connect.
|
||||||
readOnly: true
|
readOnly: true
|
||||||
type: integer
|
type: integer
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this interface.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg0.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
description: FirewallMark is an optional firewall mark which is used to handle
|
description: FirewallMark is an optional firewall mark which is used to handle
|
||||||
interface traffic.
|
interface traffic.
|
||||||
@ -306,16 +309,23 @@ definitions:
|
|||||||
- $ref: '#/definitions/models.ConfigOption-string'
|
- $ref: '#/definitions/models.ConfigOption-string'
|
||||||
description: EndpointPublicKey is the endpoint public key.
|
description: EndpointPublicKey is the endpoint public key.
|
||||||
ExpiresAt:
|
ExpiresAt:
|
||||||
allOf:
|
|
||||||
- $ref: '#/definitions/models.ExpiryDate'
|
|
||||||
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format.
|
description: ExpiresAt is the expiry date of the peer in YYYY-MM-DD format.
|
||||||
An expired peer is not able to connect.
|
An expired peer is not able to connect.
|
||||||
|
type: string
|
||||||
ExtraAllowedIPs:
|
ExtraAllowedIPs:
|
||||||
description: ExtraAllowedIPs is a list of additional allowed IP subnets for
|
description: ExtraAllowedIPs is a list of additional allowed IP subnets for
|
||||||
the peer. These allowed IP subnets are added on the server side.
|
the peer. These allowed IP subnets are added on the server side.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
Filename:
|
||||||
|
description: |-
|
||||||
|
Filename is the name of the config file for this peer.
|
||||||
|
This value is read only and is not settable by the user.
|
||||||
|
example: wg_peer_x.conf
|
||||||
|
maxLength: 21
|
||||||
|
readOnly: true
|
||||||
|
type: string
|
||||||
FirewallMark:
|
FirewallMark:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/models.ConfigOption-uint32'
|
- $ref: '#/definitions/models.ConfigOption-uint32'
|
||||||
@ -549,7 +559,6 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- Identifier
|
- Identifier
|
||||||
- IsAdmin
|
|
||||||
type: object
|
type: object
|
||||||
models.UserInformation:
|
models.UserInformation:
|
||||||
properties:
|
properties:
|
||||||
@ -739,6 +748,8 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
put:
|
put:
|
||||||
|
description: This endpoint updates an existing interface with the provided data.
|
||||||
|
All required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleUpdatePut
|
operationId: interfaces_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface identifier.
|
- description: The interface identifier.
|
||||||
@ -786,6 +797,8 @@ paths:
|
|||||||
- Interfaces
|
- Interfaces
|
||||||
/interface/new:
|
/interface/new:
|
||||||
post:
|
post:
|
||||||
|
description: This endpoint creates a new interface with the provided data. All
|
||||||
|
required fields must be filled (e.g. name, private key, public key, ...).
|
||||||
operationId: interfaces_handleCreatePost
|
operationId: interfaces_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The interface data.
|
- description: The interface data.
|
||||||
@ -826,6 +839,35 @@ paths:
|
|||||||
summary: Create a new interface record.
|
summary: Create a new interface record.
|
||||||
tags:
|
tags:
|
||||||
- Interfaces
|
- Interfaces
|
||||||
|
/interface/prepare:
|
||||||
|
get:
|
||||||
|
description: This endpoint returns a new interface with default values (fresh
|
||||||
|
key pair, valid name, new IP address pool, ...).
|
||||||
|
operationId: interfaces_handlePrepareGet
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Interface'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new interface record.
|
||||||
|
tags:
|
||||||
|
- Interfaces
|
||||||
/metrics/by-interface/{id}:
|
/metrics/by-interface/{id}:
|
||||||
get:
|
get:
|
||||||
operationId: metrics_handleMetricsForInterfaceGet
|
operationId: metrics_handleMetricsForInterfaceGet
|
||||||
@ -1015,7 +1057,8 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
put:
|
put:
|
||||||
description: Only admins can update existing records.
|
description: Only admins can update existing records. The peer record must contain
|
||||||
|
all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleUpdatePut
|
operationId: peers_handleUpdatePut
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer identifier.
|
- description: The peer identifier.
|
||||||
@ -1127,7 +1170,8 @@ paths:
|
|||||||
- Peers
|
- Peers
|
||||||
/peer/new:
|
/peer/new:
|
||||||
post:
|
post:
|
||||||
description: Only admins can create new records.
|
description: Only admins can create new records. The peer record must contain
|
||||||
|
all required fields (e.g., public key, allowed IPs).
|
||||||
operationId: peers_handleCreatePost
|
operationId: peers_handleCreatePost
|
||||||
parameters:
|
parameters:
|
||||||
- description: The peer data.
|
- description: The peer data.
|
||||||
@ -1168,6 +1212,49 @@ paths:
|
|||||||
summary: Create a new peer record.
|
summary: Create a new peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peers
|
- Peers
|
||||||
|
/peer/prepare/{id}:
|
||||||
|
get:
|
||||||
|
description: This endpoint is used to prepare a new peer record. The returned
|
||||||
|
data contains a fresh key pair and valid ip address.
|
||||||
|
operationId: peers_handlePrepareGet
|
||||||
|
parameters:
|
||||||
|
- description: The interface identifier.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Peer'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"403":
|
||||||
|
description: Forbidden
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.Error'
|
||||||
|
security:
|
||||||
|
- BasicAuth: []
|
||||||
|
summary: Prepare a new peer record for the given WireGuard interface.
|
||||||
|
tags:
|
||||||
|
- Peers
|
||||||
/provisioning/data/peer-config:
|
/provisioning/data/peer-config:
|
||||||
get:
|
get:
|
||||||
description: Normal users can only access their own record. Admins can access
|
description: Normal users can only access their own record. Admins can access
|
||||||
|
214
internal/app/api/core/middleware/cors/middleware.go
Normal file
214
internal/app/api/core/middleware/cors/middleware.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new CORS middleware. The CORS middleware
|
||||||
|
// adds Cross-Origin Resource Sharing headers to the response. This middleware should
|
||||||
|
// be used to allow cross-origin requests to your server.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
|
||||||
|
varyHeaders string // precomputed Vary header
|
||||||
|
allOrigins bool // all origins are allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new CORS middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
// set vary headers
|
||||||
|
if m.o.allowPrivateNetworks {
|
||||||
|
m.varyHeaders = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network"
|
||||||
|
} else {
|
||||||
|
m.varyHeaders = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedOrigins) == 1 && m.o.allowedOrigins[0] == "*" {
|
||||||
|
m.allOrigins = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the CORS middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle preflight requests and stop the chain as some other
|
||||||
|
// middleware may not handle OPTIONS requests correctly.
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
|
||||||
|
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
|
||||||
|
m.handlePreflight(w, r)
|
||||||
|
w.WriteHeader(http.StatusNoContent) // always return 204 No Content
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle normal CORS requests
|
||||||
|
m.handleNormal(w, r)
|
||||||
|
next.ServeHTTP(w, r) // execute the next handler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// region internal-helpers
|
||||||
|
|
||||||
|
// handlePreflight handles preflight requests. If the request was successful, this function will
|
||||||
|
// write the CORS headers and return. If the request was not successful, this function will
|
||||||
|
// not add any CORS headers and return - thus the CORS request is considered invalid.
|
||||||
|
func (m *Middleware) handlePreflight(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Always set Vary headers
|
||||||
|
// see https://github.com/rs/cors/issues/10,
|
||||||
|
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
|
||||||
|
w.Header().Add("Vary", m.varyHeaders)
|
||||||
|
|
||||||
|
// check origin
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return // not a valid CORS request
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.originAllowed(origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check method
|
||||||
|
reqMethod := r.Header.Get("Access-Control-Request-Method")
|
||||||
|
if !m.methodAllowed(reqMethod) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check headers
|
||||||
|
reqHeaders := r.Header.Get("Access-Control-Request-Headers")
|
||||||
|
if !m.headersAllowed(reqHeaders) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set CORS headers for the successful preflight request
|
||||||
|
if m.allOrigins {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin) // return original origin
|
||||||
|
}
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", reqMethod)
|
||||||
|
if reqHeaders != "" {
|
||||||
|
// Spec says: Since the list of headers can be unbounded, simply returning supported headers
|
||||||
|
// from Access-Control-Request-Headers can be enough
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", reqHeaders)
|
||||||
|
}
|
||||||
|
if m.o.allowCredentials {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
if m.o.allowPrivateNetworks && r.Header.Get("Access-Control-Request-Private-Network") == "true" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Private-Network", "true")
|
||||||
|
}
|
||||||
|
if m.o.maxAge > 0 {
|
||||||
|
w.Header().Set("Access-Control-Max-Age", strconv.Itoa(m.o.maxAge))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNormal handles normal CORS requests. If the request was successful, this function will
|
||||||
|
// write the CORS headers to the response. If the request was not successful, this function will
|
||||||
|
// not add any CORS headers to the response. In this case, the CORS request is considered invalid.
|
||||||
|
func (m *Middleware) handleNormal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Always set Vary headers
|
||||||
|
// see https://github.com/rs/cors/issues/10,
|
||||||
|
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
|
||||||
|
w.Header().Add("Vary", "Origin")
|
||||||
|
|
||||||
|
// check origin
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin == "" {
|
||||||
|
return // not a valid CORS request
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.originAllowed(origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check method
|
||||||
|
if !m.methodAllowed(r.Method) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// set CORS headers for the successful CORS request
|
||||||
|
if m.allOrigins {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin) // return original origin
|
||||||
|
}
|
||||||
|
if len(m.o.exposedHeaders) > 0 {
|
||||||
|
w.Header().Set("Access-Control-Expose-Headers", strings.Join(m.o.exposedHeaders, ", "))
|
||||||
|
}
|
||||||
|
if m.o.allowCredentials {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) originAllowed(origin string) bool {
|
||||||
|
if len(m.o.allowedOrigins) == 1 && m.o.allowedOrigins[0] == "*" {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// check simple origins
|
||||||
|
if slices.Contains(m.o.allowedOrigins, origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// check wildcard origins
|
||||||
|
for _, allowedOrigin := range m.o.allowedOriginPatterns {
|
||||||
|
if allowedOrigin.match(origin) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) methodAllowed(method string) bool {
|
||||||
|
if method == http.MethodOptions {
|
||||||
|
return true // preflight request is always allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedMethods) == 1 && m.o.allowedMethods[0] == "*" {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(m.o.allowedMethods, method) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) headersAllowed(headers string) bool {
|
||||||
|
if headers == "" {
|
||||||
|
return true // no headers are requested
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.o.allowedHeaders) == 0 {
|
||||||
|
return false // no headers are allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.o.allowedHeaders["*"]; ok {
|
||||||
|
return true // everything is allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// split headers by comma (according to definition, the headers are sorted and in lowercase)
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers
|
||||||
|
for header := range strings.SplitSeq(headers, ",") {
|
||||||
|
if _, ok := m.o.allowedHeaders[strings.TrimSpace(header)]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion internal-helpers
|
101
internal/app/api/core/middleware/cors/middleware_test.go
Normal file
101
internal/app/api/core/middleware/cors/middleware_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiddleware_New(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("*"))
|
||||||
|
|
||||||
|
if len(m.varyHeaders) == 0 {
|
||||||
|
t.Errorf("expected vary headers to be populated, got %v", m.varyHeaders)
|
||||||
|
}
|
||||||
|
if !m.allOrigins {
|
||||||
|
t.Errorf("expected allOrigins to be true, got %v", m.allOrigins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_normal(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://example.com", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Result().StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("expected status code 200, got %d", w.Result().StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Header().Get("Access-Control-Allow-Origin") != "http://example.com" {
|
||||||
|
t.Errorf("expected Access-Control-Allow-Origin to be 'http://example.com', got %s",
|
||||||
|
w.Header().Get("Access-Control-Allow-Origin"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_preflight(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodOptions, "http://example.com", nil)
|
||||||
|
req.Header.Set("Origin", "http://example.com")
|
||||||
|
req.Header.Set("Access-Control-Request-Method", http.MethodGet)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Result().StatusCode != http.StatusNoContent {
|
||||||
|
t.Errorf("expected status code 204, got %d", w.Result().StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Header().Get("Access-Control-Allow-Origin") != "http://example.com" {
|
||||||
|
t.Errorf("expected Access-Control-Allow-Origin to be 'http://example.com', got %s",
|
||||||
|
w.Header().Get("Access-Control-Allow-Origin"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_originAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedOrigins("http://example.com"))
|
||||||
|
|
||||||
|
if !m.originAllowed("http://example.com") {
|
||||||
|
t.Errorf("expected origin 'http://example.com' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.originAllowed("http://notallowed.com") {
|
||||||
|
t.Errorf("expected origin 'http://notallowed.com' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_methodAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedMethods(http.MethodGet, http.MethodPost))
|
||||||
|
|
||||||
|
if !m.methodAllowed(http.MethodGet) {
|
||||||
|
t.Errorf("expected method 'GET' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.methodAllowed(http.MethodDelete) {
|
||||||
|
t.Errorf("expected method 'DELETE' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_headersAllowed(t *testing.T) {
|
||||||
|
m := New(WithAllowedHeaders("Content-Type", "Authorization"))
|
||||||
|
|
||||||
|
if !m.headersAllowed("content-type, authorization") {
|
||||||
|
t.Errorf("expected headers 'Content-Type, Authorization' to be allowed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.headersAllowed("x-custom-header") {
|
||||||
|
t.Errorf("expected header 'X-Custom-Header' to be not allowed")
|
||||||
|
}
|
||||||
|
}
|
133
internal/app/api/core/middleware/cors/options.go
Normal file
133
internal/app/api/core/middleware/cors/options.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type void struct{}
|
||||||
|
|
||||||
|
// options is a struct that contains options for the CORS middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
allowedOrigins []string // origins without wildcards
|
||||||
|
allowedOriginPatterns []wildcard // origins with wildcards
|
||||||
|
allowedMethods []string
|
||||||
|
allowedHeaders map[string]void
|
||||||
|
exposedHeaders []string // these are in addition to the CORS-safelisted response headers
|
||||||
|
allowCredentials bool
|
||||||
|
allowPrivateNetworks bool
|
||||||
|
maxAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the CORS middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithAllowedOrigins sets the allowed origins for the CORS middleware.
|
||||||
|
// If the special "*" value is present in the list, all origins will be allowed.
|
||||||
|
// An origin may contain a wildcard (*) to replace 0 or more characters
|
||||||
|
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
|
||||||
|
// Only one wildcard can be used per origin.
|
||||||
|
// By default, all origins are allowed (*).
|
||||||
|
func WithAllowedOrigins(origins ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedOrigins = nil
|
||||||
|
o.allowedOriginPatterns = nil
|
||||||
|
|
||||||
|
for _, origin := range origins {
|
||||||
|
if len(origin) > 1 && strings.Contains(origin, "*") {
|
||||||
|
o.allowedOriginPatterns = append(
|
||||||
|
o.allowedOriginPatterns,
|
||||||
|
newWildcard(origin),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
o.allowedOrigins = append(o.allowedOrigins, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowedMethods sets the allowed methods for the CORS middleware.
|
||||||
|
// By default, all methods are allowed (*).
|
||||||
|
func WithAllowedMethods(methods ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedMethods = methods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowedHeaders sets the allowed headers for the CORS middleware.
|
||||||
|
// By default, all headers are allowed (*).
|
||||||
|
func WithAllowedHeaders(headers ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowedHeaders = make(map[string]void)
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
// allowed headers are always checked in lowercase
|
||||||
|
o.allowedHeaders[strings.ToLower(header)] = void{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExposedHeaders sets the exposed headers for the CORS middleware.
|
||||||
|
// By default, no headers are exposed.
|
||||||
|
func WithExposedHeaders(headers ...string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.exposedHeaders = nil
|
||||||
|
|
||||||
|
for _, header := range headers {
|
||||||
|
o.exposedHeaders = append(o.exposedHeaders, http.CanonicalHeaderKey(header))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowCredentials sets the allow credentials option for the CORS middleware.
|
||||||
|
// This setting indicates whether the request can include user credentials like
|
||||||
|
// cookies, HTTP authentication or client side SSL certificates.
|
||||||
|
// By default, credentials are not allowed.
|
||||||
|
func WithAllowCredentials(allow bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowCredentials = allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowPrivateNetworks sets the allow private networks option for the CORS middleware.
|
||||||
|
// This setting indicates whether to accept cross-origin requests over a private network.
|
||||||
|
func WithAllowPrivateNetworks(allow bool) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.allowPrivateNetworks = allow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxAge sets the max age (in seconds) for the CORS middleware.
|
||||||
|
// The maximum age indicates how long (in seconds) the results of a preflight request
|
||||||
|
// can be cached. A value of 0 means that no Access-Control-Max-Age header is sent back,
|
||||||
|
// resulting in browsers using their default value (5s by spec).
|
||||||
|
// If you need to force a 0 max-age, set it to a negative value (ie: -1).
|
||||||
|
// By default, the max age is 7200 seconds.
|
||||||
|
func WithMaxAge(age int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxAge = age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
allowedOrigins: []string{"*"},
|
||||||
|
allowedMethods: []string{
|
||||||
|
http.MethodHead, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete,
|
||||||
|
},
|
||||||
|
allowedHeaders: map[string]void{"*": {}},
|
||||||
|
exposedHeaders: nil,
|
||||||
|
allowCredentials: false,
|
||||||
|
allowPrivateNetworks: false,
|
||||||
|
maxAge: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
96
internal/app/api/core/middleware/cors/options_test.go
Normal file
96
internal/app/api/core/middleware/cors/options_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithAllowedOrigins(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
origins []string
|
||||||
|
wantNormal []string
|
||||||
|
wantWildcard []wildcard
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No origins",
|
||||||
|
origins: []string{},
|
||||||
|
wantNormal: nil,
|
||||||
|
wantWildcard: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single origin",
|
||||||
|
origins: []string{"http://example.com"},
|
||||||
|
wantNormal: []string{"http://example.com"},
|
||||||
|
wantWildcard: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard origin",
|
||||||
|
origins: []string{"http://*.example.com"},
|
||||||
|
wantNormal: nil,
|
||||||
|
wantWildcard: []wildcard{newWildcard("http://*.example.com")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowedOrigins(tt.origins...))
|
||||||
|
if !slices.Equal(o.allowedOrigins, tt.wantNormal) {
|
||||||
|
t.Errorf("got %v, want %v", o, tt.wantNormal)
|
||||||
|
}
|
||||||
|
if !slices.Equal(o.allowedOriginPatterns, tt.wantWildcard) {
|
||||||
|
t.Errorf("got %v, want %v", o, tt.wantWildcard)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowedMethods(t *testing.T) {
|
||||||
|
methods := []string{http.MethodGet, http.MethodPost}
|
||||||
|
o := newOptions(WithAllowedMethods(methods...))
|
||||||
|
if !slices.Equal(o.allowedMethods, methods) {
|
||||||
|
t.Errorf("got %v, want %v", o.allowedMethods, methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowedHeaders(t *testing.T) {
|
||||||
|
headers := []string{"Content-Type", "Authorization"}
|
||||||
|
o := newOptions(WithAllowedHeaders(headers...))
|
||||||
|
expectedHeaders := map[string]void{"content-type": {}, "authorization": {}}
|
||||||
|
if !maps.Equal(o.allowedHeaders, expectedHeaders) {
|
||||||
|
t.Errorf("got %v, want %v", o.allowedHeaders, expectedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithExposedHeaders(t *testing.T) {
|
||||||
|
headers := []string{"X-Custom-Header"}
|
||||||
|
o := newOptions(WithExposedHeaders(headers...))
|
||||||
|
expectedHeaders := []string{http.CanonicalHeaderKey("X-Custom-Header")}
|
||||||
|
if !slices.Equal(o.exposedHeaders, expectedHeaders) {
|
||||||
|
t.Errorf("got %v, want %v", o.exposedHeaders, expectedHeaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowCredentials(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowCredentials(true))
|
||||||
|
if !o.allowCredentials {
|
||||||
|
t.Errorf("got %v, want %v", o.allowCredentials, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithAllowPrivateNetworks(t *testing.T) {
|
||||||
|
o := newOptions(WithAllowPrivateNetworks(true))
|
||||||
|
if !o.allowPrivateNetworks {
|
||||||
|
t.Errorf("got %v, want %v", o.allowPrivateNetworks, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithMaxAge(t *testing.T) {
|
||||||
|
maxAge := 3600
|
||||||
|
o := newOptions(WithMaxAge(maxAge))
|
||||||
|
if o.maxAge != maxAge {
|
||||||
|
t.Errorf("got %v, want %v", o.maxAge, maxAge)
|
||||||
|
}
|
||||||
|
}
|
33
internal/app/api/core/middleware/cors/wildcard.go
Normal file
33
internal/app/api/core/middleware/cors/wildcard.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// wildcard is a type that represents a wildcard string.
|
||||||
|
// This type allows faster matching of strings with a wildcard
|
||||||
|
// in comparison to using regex.
|
||||||
|
type wildcard struct {
|
||||||
|
prefix string
|
||||||
|
suffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// match returns true if the string s has the prefix and suffix of the wildcard.
|
||||||
|
func (w wildcard) match(s string) bool {
|
||||||
|
return len(s) >= len(w.prefix)+len(w.suffix) &&
|
||||||
|
strings.HasPrefix(s, w.prefix) &&
|
||||||
|
strings.HasSuffix(s, w.suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWildcard(s string) wildcard {
|
||||||
|
if i := strings.IndexByte(s, '*'); i >= 0 {
|
||||||
|
return wildcard{
|
||||||
|
prefix: s[:i],
|
||||||
|
suffix: s[i+1:],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback, usually this case should not happen
|
||||||
|
return wildcard{
|
||||||
|
prefix: s,
|
||||||
|
suffix: "",
|
||||||
|
}
|
||||||
|
}
|
94
internal/app/api/core/middleware/cors/wildcard_test.go
Normal file
94
internal/app/api/core/middleware/cors/wildcard_test.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package cors
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestWildcardMatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
wildcard wildcard
|
||||||
|
input string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Match with prefix and suffix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "http://sub.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with different prefix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "https://sub.example.com",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with different suffix",
|
||||||
|
wildcard: newWildcard("http://*.example.com"),
|
||||||
|
input: "http://sub.example.org",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Match with empty suffix",
|
||||||
|
wildcard: newWildcard("http://*"),
|
||||||
|
input: "http://example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Match with empty prefix",
|
||||||
|
wildcard: newWildcard("*.example.com"),
|
||||||
|
input: "sub.example.com",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No match with empty prefix and different suffix",
|
||||||
|
wildcard: newWildcard("*.example.com"),
|
||||||
|
input: "sub.example.org",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.wildcard.match(tt.input); got != tt.expected {
|
||||||
|
t.Errorf("wildcard.match(%s) = %v, want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWildcard(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected wildcard
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Wildcard with prefix and suffix",
|
||||||
|
input: "http://*.example.com",
|
||||||
|
expected: wildcard{prefix: "http://", suffix: ".example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with empty suffix",
|
||||||
|
input: "http://*",
|
||||||
|
expected: wildcard{prefix: "http://", suffix: ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wildcard with empty prefix",
|
||||||
|
input: "*.example.com",
|
||||||
|
expected: wildcard{prefix: "", suffix: ".example.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No wildcard character",
|
||||||
|
input: "http://example.com",
|
||||||
|
expected: wildcard{prefix: "http://example.com", suffix: ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := newWildcard(tt.input); got != tt.expected {
|
||||||
|
t.Errorf("newWildcard(%s) = %v, want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
137
internal/app/api/core/middleware/csrf/middleware.go
Normal file
137
internal/app/api/core/middleware/csrf/middleware.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextValueIdentifier is the context value identifier for the CSRF token.
|
||||||
|
// The token is only stored in the context if the RefreshToken function was called before.
|
||||||
|
const ContextValueIdentifier = "_csrf_token"
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new CSRF middleware. The CSRF middleware
|
||||||
|
// can be used to mitigate Cross-Site Request Forgery attacks.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new CSRF middleware with the provided options.
|
||||||
|
func New(sessionReader SessionReader, sessionWriter SessionWriter, opts ...Option) *Middleware {
|
||||||
|
opts = append(opts, withSessionReader(sessionReader), withSessionWriter(sessionWriter))
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForPRNG()
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the CSRF middleware handler. This middleware validates the CSRF token and calls the specified
|
||||||
|
// error handler if an invalid CSRF token was found.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if slices.Contains(m.o.ignoreMethods, r.Method) {
|
||||||
|
next.ServeHTTP(w, r) // skip CSRF check for ignored methods
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the token from the request
|
||||||
|
token := m.o.tokenGetter(r)
|
||||||
|
storedToken := m.o.sessionGetter(r)
|
||||||
|
|
||||||
|
if !tokenEqual(token, storedToken) {
|
||||||
|
m.o.errCallback(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r) // execute the next handler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken generates a new CSRF Token and stores it in the session. The token is also passed to subsequent handlers
|
||||||
|
// via the context value ContextValueIdentifier.
|
||||||
|
func (m *Middleware) RefreshToken(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if GetToken(r.Context()) != "" {
|
||||||
|
// token already generated higher up in the chain
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a new token
|
||||||
|
token := generateToken(m.o.tokenLength)
|
||||||
|
key := generateToken(m.o.tokenLength)
|
||||||
|
|
||||||
|
// mask the token
|
||||||
|
maskedToken := maskToken(token, key)
|
||||||
|
encodedToken := encodeToken(maskedToken)
|
||||||
|
|
||||||
|
// pass the token down the chain via the context
|
||||||
|
r = r.WithContext(setToken(r.Context(), encodedToken))
|
||||||
|
|
||||||
|
// store the token in the session
|
||||||
|
m.o.sessionWriter(r, encodedToken)
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// region token-access
|
||||||
|
|
||||||
|
// GetToken retrieves the CSRF token from the given context. Ensure that the RefreshToken function was called before,
|
||||||
|
// otherwise, no token is populated in the context.
|
||||||
|
func GetToken(ctx context.Context) string {
|
||||||
|
token, ok := ctx.Value(ContextValueIdentifier).(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion token-access
|
||||||
|
|
||||||
|
// region internal-helpers
|
||||||
|
|
||||||
|
func setToken(ctx context.Context, token string) context.Context {
|
||||||
|
return context.WithValue(ctx, ContextValueIdentifier, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultTokenGetter is the default token getter function for the CSRF middleware.
|
||||||
|
// It checks the request form values, URL query parameters, and headers for the CSRF token.
|
||||||
|
// The order of precedence is:
|
||||||
|
// 1. Header "X-CSRF-TOKEN"
|
||||||
|
// 2. Header "X-XSRF-TOKEN"
|
||||||
|
// 3. URL query parameter "_csrf"
|
||||||
|
// 4. Form value "_csrf"
|
||||||
|
func defaultTokenGetter(r *http.Request) string {
|
||||||
|
if t := r.Header.Get("X-CSRF-TOKEN"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.Header.Get("X-XSRF-TOKEN"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.URL.Query().Get("_csrf"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if t := r.FormValue("_csrf"); len(t) > 0 {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultErrorHandler is the default error handler function for the CSRF middleware.
|
||||||
|
// It writes a 403 Forbidden response.
|
||||||
|
func defaultErrorHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
http.Error(w, "CSRF token mismatch", http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion internal-helpers
|
251
internal/app/api/core/middleware/csrf/middleware_test.go
Normal file
251
internal/app/api/core/middleware/csrf/middleware_test.go
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/h44z/wg-portal/internal/app/api/core/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiddleware_Handler(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
token string
|
||||||
|
wantStatus int
|
||||||
|
}{
|
||||||
|
{"ValidToken", "POST", "stored-token", http.StatusOK},
|
||||||
|
{"ValidToken2", "PUT", "stored-token", http.StatusOK},
|
||||||
|
{"ValidToken3", "GET", "stored-token", http.StatusOK},
|
||||||
|
{"InvalidToken", "POST", "invalid-token", http.StatusForbidden},
|
||||||
|
{"IgnoredMethod", "GET", "", http.StatusOK},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(tt.method, "/", nil)
|
||||||
|
req.Header.Set("X-CSRF-TOKEN", tt.token)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != tt.wantStatus {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, tt.wantStatus)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := GetToken(r.Context())
|
||||||
|
if token == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("RefreshToken() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken_chained(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
tokenWrites := 0
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
tokenWrites++
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.RefreshToken(m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := GetToken(r.Context())
|
||||||
|
if token == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("RefreshToken() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenWrites != 1 {
|
||||||
|
t.Errorf("RefreshToken() wrote token to session more than once: %d", tokenWrites)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_RefreshToken_Handler(t *testing.T) {
|
||||||
|
sessionToken := ""
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
// simulate two requests: first one GET request with the RefreshToken handler, the next one is a PUT request with
|
||||||
|
// the token from the first request added as X-CSRF-TOKEN header
|
||||||
|
|
||||||
|
// first request
|
||||||
|
retrievedToken := ""
|
||||||
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := m.RefreshToken(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
retrievedToken = GetToken(r.Context())
|
||||||
|
if retrievedToken == "" {
|
||||||
|
t.Errorf("RefreshToken() did not set token in context")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}))
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
if status := rr.Code; status != http.StatusAccepted {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusAccepted)
|
||||||
|
}
|
||||||
|
if retrievedToken == "" {
|
||||||
|
t.Errorf("no token retrieved")
|
||||||
|
}
|
||||||
|
if retrievedToken != sessionToken {
|
||||||
|
t.Errorf("token in context does not match token in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// second request
|
||||||
|
req = httptest.NewRequest("PUT", "/", nil)
|
||||||
|
req.Header.Set("X-CSRF-TOKEN", retrievedToken)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
handler = m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_FormBody(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyData, err := request.BodyString(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Handler() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
// ensure that the body is empty - ParseForm() should have been called before by the CSRF middleware
|
||||||
|
if bodyData != "" {
|
||||||
|
t.Errorf("Handler() bodyData = %s, want empty", bodyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.FormValue("_csrf") != "stored-token" {
|
||||||
|
t.Errorf("Handler() _csrf = %s, want %s", r.FormValue("_csrf"), "stored-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", nil)
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Form = make(map[string][]string)
|
||||||
|
req.Form.Add("_csrf", "stored-token")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Handler_FormBodyAvailable(t *testing.T) {
|
||||||
|
sessionToken := "stored-token"
|
||||||
|
sessionReader := func(r *http.Request) string {
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
sessionWriter := func(r *http.Request, token string) {
|
||||||
|
sessionToken = token
|
||||||
|
}
|
||||||
|
m := New(sessionReader, sessionWriter)
|
||||||
|
|
||||||
|
handler := m.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyData, err := request.BodyString(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Handler() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
// ensure that the body is not empty, as the CSRF middleware should not have read the body
|
||||||
|
if bodyData != "the original body" {
|
||||||
|
t.Errorf("Handler() bodyData = %s, want %s", bodyData, "the original body")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the token is available in the form values (from query parameters)
|
||||||
|
if r.FormValue("_csrf") != "stored-token" {
|
||||||
|
t.Errorf("Handler() _csrf = %s, want %s", r.FormValue("_csrf"), "stored-token")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/?_csrf=stored-token", nil)
|
||||||
|
req.Header.Set("Content-Type", "text/plain")
|
||||||
|
req.Body = io.NopCloser(strings.NewReader("the original body"))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("Handler() status = %d, want %d", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
88
internal/app/api/core/middleware/csrf/options.go
Normal file
88
internal/app/api/core/middleware/csrf/options.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type SessionReader func(r *http.Request) string
|
||||||
|
type SessionWriter func(r *http.Request, token string)
|
||||||
|
|
||||||
|
// options is a struct that contains options for the CSRF middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
tokenLength int
|
||||||
|
ignoreMethods []string
|
||||||
|
|
||||||
|
errCallbackOverride bool
|
||||||
|
errCallback func(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
tokenGetterOverride bool
|
||||||
|
tokenGetter func(r *http.Request) string
|
||||||
|
|
||||||
|
sessionGetter SessionReader
|
||||||
|
sessionWriter SessionWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the CSRF middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithTokenLength is a method that sets the token length for the CSRF middleware.
|
||||||
|
// The default value is 32.
|
||||||
|
func WithTokenLength(length int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tokenLength = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorCallback is a method that sets the error callback function for the CSRF middleware.
|
||||||
|
// The error callback function is called when the CSRF token is invalid.
|
||||||
|
// The default behavior is to write a 403 Forbidden response.
|
||||||
|
func WithErrorCallback(fn func(w http.ResponseWriter, r *http.Request)) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.errCallback = fn
|
||||||
|
o.errCallbackOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTokenGetter is a method that sets the token getter function for the CSRF middleware.
|
||||||
|
// The token getter function is called to get the CSRF token from the request.
|
||||||
|
// The default behavior is to get the token from the "X-CSRF-Token" header.
|
||||||
|
func WithTokenGetter(fn func(r *http.Request) string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.tokenGetter = fn
|
||||||
|
o.tokenGetterOverride = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withSessionReader is a method that sets the session reader function for the CSRF middleware.
|
||||||
|
// The session reader function is called to get the CSRF token from the session.
|
||||||
|
func withSessionReader(fn SessionReader) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.sessionGetter = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withSessionWriter is a method that sets the session writer function for the CSRF middleware.
|
||||||
|
// The session writer function is called to write the CSRF token to the session.
|
||||||
|
func withSessionWriter(fn SessionWriter) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.sessionWriter = fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
tokenLength: 32,
|
||||||
|
ignoreMethods: []string{"GET", "HEAD", "OPTIONS"},
|
||||||
|
errCallbackOverride: false,
|
||||||
|
errCallback: defaultErrorHandler,
|
||||||
|
tokenGetterOverride: false,
|
||||||
|
tokenGetter: defaultTokenGetter,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
75
internal/app/api/core/middleware/csrf/options_test.go
Normal file
75
internal/app/api/core/middleware/csrf/options_test.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithTokenLength(t *testing.T) {
|
||||||
|
o := newOptions(WithTokenLength(64))
|
||||||
|
if o.tokenLength != 64 {
|
||||||
|
t.Errorf("WithTokenLength() = %d, want %d", o.tokenLength, 64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithErrorCallback(t *testing.T) {
|
||||||
|
callback := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
}
|
||||||
|
o := newOptions(WithErrorCallback(callback))
|
||||||
|
if !o.errCallbackOverride {
|
||||||
|
t.Errorf("WithErrorCallback() did not set errCallbackOverride to true")
|
||||||
|
}
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("WithErrorCallback() did not set errCallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithTokenGetter(t *testing.T) {
|
||||||
|
getter := func(r *http.Request) string {
|
||||||
|
return "test-token"
|
||||||
|
}
|
||||||
|
o := newOptions(WithTokenGetter(getter))
|
||||||
|
if !o.tokenGetterOverride {
|
||||||
|
t.Errorf("WithTokenGetter() did not set tokenGetterOverride to true")
|
||||||
|
}
|
||||||
|
if o.tokenGetter == nil {
|
||||||
|
t.Errorf("WithTokenGetter() did not set tokenGetter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSessionReader(t *testing.T) {
|
||||||
|
reader := func(r *http.Request) string {
|
||||||
|
return "session-token"
|
||||||
|
}
|
||||||
|
o := newOptions(withSessionReader(reader))
|
||||||
|
if o.sessionGetter == nil {
|
||||||
|
t.Errorf("withSessionReader() did not set sessionGetter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSessionWriter(t *testing.T) {
|
||||||
|
writer := func(r *http.Request, token string) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
o := newOptions(withSessionWriter(writer))
|
||||||
|
if o.sessionWriter == nil {
|
||||||
|
t.Errorf("withSessionWriter() did not set sessionWriter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewOptionsDefaults(t *testing.T) {
|
||||||
|
o := newOptions()
|
||||||
|
if o.tokenLength != 32 {
|
||||||
|
t.Errorf("newOptions() default tokenLength = %d, want %d", o.tokenLength, 32)
|
||||||
|
}
|
||||||
|
if len(o.ignoreMethods) != 3 {
|
||||||
|
t.Errorf("newOptions() default ignoreMethods length = %d, want %d", len(o.ignoreMethods), 3)
|
||||||
|
}
|
||||||
|
if o.errCallback == nil {
|
||||||
|
t.Errorf("newOptions() default errCallback is nil")
|
||||||
|
}
|
||||||
|
if o.tokenGetter == nil {
|
||||||
|
t.Errorf("newOptions() default tokenGetter is nil")
|
||||||
|
}
|
||||||
|
}
|
90
internal/app/api/core/middleware/csrf/token.go
Normal file
90
internal/app/api/core/middleware/csrf/token.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkForPRNG is a function that checks if a cryptographically secure PRNG is available.
|
||||||
|
// If it is not available, the function panics.
|
||||||
|
func checkForPRNG() {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := io.ReadFull(rand.Reader, buf)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateToken is a function that generates a secure random CSRF token.
|
||||||
|
func generateToken(length int) []byte {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
|
||||||
|
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeToken is a function that encodes a token to a base64 string.
|
||||||
|
func encodeToken(token []byte) string {
|
||||||
|
return base64.URLEncoding.EncodeToString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeToken is a function that decodes a base64 string to a token.
|
||||||
|
func decodeToken(token string) ([]byte, error) {
|
||||||
|
return base64.URLEncoding.DecodeString(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskToken is a function that masks a token with a given key.
|
||||||
|
// The returned byte slice contains the key + the masked token.
|
||||||
|
// The key needs to have the same length as the token, otherwise the function panics.
|
||||||
|
// So the resulting slice has a length of len(token) * 2.
|
||||||
|
func maskToken(token, key []byte) []byte {
|
||||||
|
if len(token) != len(key) {
|
||||||
|
panic("token and key must have the same length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// masked contains the key in the first half and the XOR masked token in the second half
|
||||||
|
tokenLength := len(token)
|
||||||
|
masked := make([]byte, tokenLength*2)
|
||||||
|
for i := 0; i < len(token); i++ {
|
||||||
|
masked[i] = key[i]
|
||||||
|
masked[i+tokenLength] = token[i] ^ key[i] // XOR mask
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmaskToken is a function that unmask a token which contains the key in the first half.
|
||||||
|
// The returned byte slice contains the unmasked token, it has exactly half the length of the input slice.
|
||||||
|
func unmaskToken(masked []byte) []byte {
|
||||||
|
tokenLength := len(masked) / 2
|
||||||
|
token := make([]byte, tokenLength)
|
||||||
|
for i := 0; i < tokenLength; i++ {
|
||||||
|
token[i] = masked[i] ^ masked[i+tokenLength] // XOR unmask
|
||||||
|
}
|
||||||
|
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenEqual is a function that compares two tokens for equality.
|
||||||
|
func tokenEqual(a, b string) bool {
|
||||||
|
decodedA, err := decodeToken(a)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
decodedB, err := decodeToken(b)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
unmaskedA := unmaskToken(decodedA)
|
||||||
|
unmaskedB := unmaskToken(decodedB)
|
||||||
|
|
||||||
|
return slices.Equal(unmaskedA, unmaskedB)
|
||||||
|
}
|
81
internal/app/api/core/middleware/csrf/token_test.go
Normal file
81
internal/app/api/core/middleware/csrf/token_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package csrf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckForPRNG(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
t.Errorf("checkForPRNG() panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
checkForPRNG()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateToken(t *testing.T) {
|
||||||
|
length := 32
|
||||||
|
token := generateToken(length)
|
||||||
|
if len(token) != length {
|
||||||
|
t.Errorf("generateToken() returned token of length %d, expected %d", len(token), length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
encoded := encodeToken(token)
|
||||||
|
expected := base64.URLEncoding.EncodeToString(token)
|
||||||
|
if encoded != expected {
|
||||||
|
t.Errorf("encodeToken() = %v, want %v", encoded, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeToken(t *testing.T) {
|
||||||
|
token := "dGVzdHRva2Vu"
|
||||||
|
expected := []byte("testtoken")
|
||||||
|
decoded, err := decodeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("decodeToken() error = %v", err)
|
||||||
|
}
|
||||||
|
if string(decoded) != string(expected) {
|
||||||
|
t.Errorf("decodeToken() = %v, want %v", decoded, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaskToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
key := []byte("keykeykey")
|
||||||
|
masked := maskToken(token, key)
|
||||||
|
if len(masked) != len(token)*2 {
|
||||||
|
t.Errorf("maskToken() returned masked token of length %d, expected %d", len(masked), len(token)*2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmaskToken(t *testing.T) {
|
||||||
|
token := []byte("testtoken")
|
||||||
|
key := []byte("keykeykey")
|
||||||
|
masked := maskToken(token, key)
|
||||||
|
unmasked := unmaskToken(masked)
|
||||||
|
if string(unmasked) != string(token) {
|
||||||
|
t.Errorf("unmaskToken() = %v, want %v", unmasked, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenEqual(t *testing.T) {
|
||||||
|
tokenA := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}))
|
||||||
|
tokenB := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x04, 0x05, 0x06}))
|
||||||
|
if !tokenEqual(tokenA, tokenB) {
|
||||||
|
t.Errorf("tokenEqual() = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenC := encodeToken(maskToken([]byte{0x01, 0x02, 0x03}, []byte{0x07, 0x08, 0x09}))
|
||||||
|
if !tokenEqual(tokenA, tokenC) {
|
||||||
|
t.Errorf("tokenEqual() = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenD := encodeToken(maskToken([]byte{0x09, 0x02, 0x03}, []byte{0x04, 0x05, 0x06}))
|
||||||
|
if tokenEqual(tokenA, tokenD) {
|
||||||
|
t.Errorf("tokenEqual() = true, want false")
|
||||||
|
}
|
||||||
|
}
|
199
internal/app/api/core/middleware/logging/middleware.go
Normal file
199
internal/app/api/core/middleware/logging/middleware.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogLevel is an enumeration of the different log levels.
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogLevelDebug LogLevel = iota
|
||||||
|
LogLevelInfo
|
||||||
|
LogLevelWarn
|
||||||
|
LogLevelError
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger is an interface that defines the methods that a logger must implement.
|
||||||
|
// This allows the logging middleware to be used with different logging libraries.
|
||||||
|
type Logger interface {
|
||||||
|
// Debugf logs a message at debug level.
|
||||||
|
Debugf(format string, args ...any)
|
||||||
|
// Infof logs a message at info level.
|
||||||
|
Infof(format string, args ...any)
|
||||||
|
// Warnf logs a message at warn level.
|
||||||
|
Warnf(format string, args ...any)
|
||||||
|
// Errorf logs a message at error level.
|
||||||
|
Errorf(format string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware is a type that creates a new logging middleware. The logging middleware
|
||||||
|
// logs information about each request.
|
||||||
|
type Middleware struct {
|
||||||
|
o options
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new logging middleware with the provided options.
|
||||||
|
func New(opts ...Option) *Middleware {
|
||||||
|
o := newOptions(opts...)
|
||||||
|
|
||||||
|
m := &Middleware{
|
||||||
|
o: o,
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns the logging middleware handler.
|
||||||
|
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ww := newWriterWrapper(w)
|
||||||
|
start := time.Now()
|
||||||
|
defer func() {
|
||||||
|
info := m.extractInfoMap(r, start, ww)
|
||||||
|
|
||||||
|
if m.o.logger == nil {
|
||||||
|
msg, args := m.buildSlogMessageAndArguments(info)
|
||||||
|
m.logMsg(msg, args...)
|
||||||
|
} else {
|
||||||
|
msg := m.buildNormalLogMessage(info)
|
||||||
|
m.logMsg(msg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) extractInfoMap(r *http.Request, start time.Time, ww *writerWrapper) map[string]any {
|
||||||
|
info := make(map[string]any)
|
||||||
|
|
||||||
|
info["method"] = r.Method
|
||||||
|
info["path"] = r.URL.Path
|
||||||
|
info["protocol"] = r.Proto
|
||||||
|
info["clientIP"] = r.Header.Get("X-Forwarded-For")
|
||||||
|
if info["clientIP"] == "" {
|
||||||
|
// If the X-Forwarded-For header is not set, use the remote address without the port number.
|
||||||
|
lastColonIndex := strings.LastIndex(r.RemoteAddr, ":")
|
||||||
|
switch lastColonIndex {
|
||||||
|
case -1:
|
||||||
|
info["clientIP"] = r.RemoteAddr
|
||||||
|
default:
|
||||||
|
info["clientIP"] = r.RemoteAddr[:lastColonIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info["userAgent"] = r.UserAgent()
|
||||||
|
info["referer"] = r.Header.Get("Referer")
|
||||||
|
info["duration"] = time.Since(start).String()
|
||||||
|
info["status"] = ww.StatusCode
|
||||||
|
info["dataLength"] = ww.WrittenBytes
|
||||||
|
|
||||||
|
if m.o.headerRequestIdKey != "" {
|
||||||
|
info["headerRequestId"] = r.Header.Get(m.o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
if m.o.contextRequestIdKey != "" {
|
||||||
|
info["contextRequestId"], _ = r.Context().Value(m.o.contextRequestIdKey).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) buildNormalLogMessage(info map[string]any) string {
|
||||||
|
switch {
|
||||||
|
case info["headerRequestId"] != nil && info["contextRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - rid=%s ctx=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["headerRequestId"], info["contextRequestId"])
|
||||||
|
case info["headerRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - rid=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["headerRequestId"])
|
||||||
|
case info["contextRequestId"] != nil:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s - ctx=%s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"],
|
||||||
|
info["contextRequestId"])
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s %s %s - %d %d - %s - %s %s %s",
|
||||||
|
info["method"], info["path"], info["protocol"],
|
||||||
|
info["status"], info["dataLength"],
|
||||||
|
info["duration"],
|
||||||
|
info["clientIP"], info["userAgent"], info["referer"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) buildSlogMessageAndArguments(info map[string]any) (message string, args []any) {
|
||||||
|
message = fmt.Sprintf("%s %s", info["method"], info["path"])
|
||||||
|
|
||||||
|
// Use a fixed order for the keys, so that the message is always the same.
|
||||||
|
// Skip method and path as they are already in the message.
|
||||||
|
keys := []string{
|
||||||
|
"protocol",
|
||||||
|
"status",
|
||||||
|
"dataLength",
|
||||||
|
"duration",
|
||||||
|
"clientIP",
|
||||||
|
"userAgent",
|
||||||
|
"referer",
|
||||||
|
"headerRequestId",
|
||||||
|
"contextRequestId",
|
||||||
|
}
|
||||||
|
for _, k := range keys {
|
||||||
|
if v, ok := info[k]; ok {
|
||||||
|
args = append(args, k, v) // only add key, value if it exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) addPrefix(message string) string {
|
||||||
|
if m.o.prefix != "" {
|
||||||
|
return m.o.prefix + " " + message
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Middleware) logMsg(message string, args ...any) {
|
||||||
|
message = m.addPrefix(message)
|
||||||
|
|
||||||
|
if m.o.logger != nil {
|
||||||
|
switch m.o.logLevel {
|
||||||
|
case LogLevelDebug:
|
||||||
|
m.o.logger.Debugf(message, args...)
|
||||||
|
case LogLevelInfo:
|
||||||
|
m.o.logger.Infof(message, args...)
|
||||||
|
case LogLevelWarn:
|
||||||
|
m.o.logger.Warnf(message, args...)
|
||||||
|
case LogLevelError:
|
||||||
|
m.o.logger.Errorf(message, args...)
|
||||||
|
default:
|
||||||
|
m.o.logger.Infof(message, args...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch m.o.logLevel {
|
||||||
|
case LogLevelDebug:
|
||||||
|
slog.Debug(message, args...)
|
||||||
|
case LogLevelInfo:
|
||||||
|
slog.Info(message, args...)
|
||||||
|
case LogLevelWarn:
|
||||||
|
slog.Warn(message, args...)
|
||||||
|
case LogLevelError:
|
||||||
|
slog.Error(message, args...)
|
||||||
|
default:
|
||||||
|
slog.Info(message, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
148
internal/app/api/core/middleware/logging/middleware_test.go
Normal file
148
internal/app/api/core/middleware/logging/middleware_test.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockLogger struct {
|
||||||
|
messages []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockLogger) Debugf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "DEBUG: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Infof(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "INFO: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Warnf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "WARN: "+format)
|
||||||
|
}
|
||||||
|
func (m *mockLogger) Errorf(format string, _ ...any) {
|
||||||
|
m.messages = append(m.messages, "ERROR: "+format)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Normal(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusTeapot {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusTeapot, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Hello, World!"
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logger.messages) == 0 {
|
||||||
|
t.Errorf("expected log messages, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logger.messages) != 0 && !strings.Contains(logger.messages[0], "ERROR: GET /foo") {
|
||||||
|
t.Errorf("expected log message to contain request info, got %v", logger.messages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Extended(t *testing.T) {
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithContextRequestIdKey("requestId"), WithHeaderRequestIdKey("X-Request-Id")).
|
||||||
|
Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusTeapot {
|
||||||
|
t.Errorf("expected status code to be %v, got %v", http.StatusTeapot, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Hello, World!"
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("expected response body to be %v, got %v", expected, rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddr(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "xhamster.com:1234"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrNoPort(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "xhamster.com"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrV6(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "[::1]:4711"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddleware_Logger_remoteAddrV6NoPort(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware := New(WithLogger(logger), WithLevel(LogLevelError)).Handler(handler)
|
||||||
|
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
|
||||||
|
req.RemoteAddr = "[::1]"
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
}
|
80
internal/app/api/core/middleware/logging/options.go
Normal file
80
internal/app/api/core/middleware/logging/options.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
// options is a struct that contains options for the logging middleware.
|
||||||
|
// It uses the functional options pattern for flexible configuration.
|
||||||
|
type options struct {
|
||||||
|
logLevel LogLevel
|
||||||
|
logger Logger
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
contextRequestIdKey string
|
||||||
|
headerRequestIdKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is a type that is used to set options for the logging middleware.
|
||||||
|
// It implements the functional options pattern.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithLevel is a method that sets the log level for the logging middleware.
|
||||||
|
// Possible values are LogLevelDebug, LogLevelInfo, LogLevelWarn, and LogLevelError.
|
||||||
|
// The default value is LogLevelInfo.
|
||||||
|
func WithLevel(level LogLevel) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logLevel = level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix is a method that sets the prefix for the logging middleware.
|
||||||
|
// If a prefix is set, it will be prepended to each log message. A space will
|
||||||
|
// be added between the prefix and the log message.
|
||||||
|
// The default value is an empty string.
|
||||||
|
func WithPrefix(prefix string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.prefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContextRequestIdKey is a method that sets the key for the request ID in the
|
||||||
|
// request context. If a key is set, the logging middleware will use this key to
|
||||||
|
// retrieve the request ID from the request context.
|
||||||
|
// The default value is an empty string, meaning the request ID will not be logged.
|
||||||
|
func WithContextRequestIdKey(key string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.contextRequestIdKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaderRequestIdKey is a method that sets the key for the request ID in the
|
||||||
|
// request headers. If a key is set, the logging middleware will use this key to
|
||||||
|
// retrieve the request ID from the request headers.
|
||||||
|
// The default value is an empty string, meaning the request ID will not be logged.
|
||||||
|
func WithHeaderRequestIdKey(key string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.headerRequestIdKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger is a method that sets the logger for the logging middleware.
|
||||||
|
// If a logger is set, the logging middleware will use this logger to log messages.
|
||||||
|
// The default logger is the structured slog logger.
|
||||||
|
func WithLogger(logger Logger) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.logger = logger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newOptions is a function that returns a new options struct with sane default values.
|
||||||
|
func newOptions(opts ...Option) options {
|
||||||
|
o := options{
|
||||||
|
logLevel: LogLevelInfo,
|
||||||
|
logger: nil,
|
||||||
|
prefix: "",
|
||||||
|
contextRequestIdKey: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&o)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
}
|
88
internal/app/api/core/middleware/logging/options_test.go
Normal file
88
internal/app/api/core/middleware/logging/options_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithLevel(t *testing.T) {
|
||||||
|
// table test to check all possible log levels
|
||||||
|
levels := []LogLevel{
|
||||||
|
LogLevelDebug,
|
||||||
|
LogLevelInfo,
|
||||||
|
LogLevelWarn,
|
||||||
|
LogLevelError,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, level := range levels {
|
||||||
|
opt := WithLevel(level)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logLevel != level {
|
||||||
|
t.Errorf("expected log level to be %v, got %v", level, o.logLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithPrefix(t *testing.T) {
|
||||||
|
prefix := "TEST"
|
||||||
|
opt := WithPrefix(prefix)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.prefix != prefix {
|
||||||
|
t.Errorf("expected prefix to be %v, got %v", prefix, o.prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithContextRequestIdKey(t *testing.T) {
|
||||||
|
key := "contextKey"
|
||||||
|
opt := WithContextRequestIdKey(key)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.contextRequestIdKey != key {
|
||||||
|
t.Errorf("expected contextRequestIdKey to be %v, got %v", key, o.contextRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithHeaderRequestIdKey(t *testing.T) {
|
||||||
|
key := "headerKey"
|
||||||
|
opt := WithHeaderRequestIdKey(key)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.headerRequestIdKey != key {
|
||||||
|
t.Errorf("expected headerRequestIdKey to be %v, got %v", key, o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithLogger(t *testing.T) {
|
||||||
|
logger := &mockLogger{}
|
||||||
|
opt := WithLogger(logger)
|
||||||
|
o := newOptions(opt)
|
||||||
|
|
||||||
|
if o.logger != logger {
|
||||||
|
t.Errorf("expected logger to be %v, got %v", logger, o.logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaults(t *testing.T) {
|
||||||
|
o := newOptions()
|
||||||
|
|
||||||
|
if o.logLevel != LogLevelInfo {
|
||||||
|
t.Errorf("expected log level to be %v, got %v", LogLevelInfo, o.logLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.logger != nil {
|
||||||
|
t.Errorf("expected logger to be nil, got %v", o.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.prefix != "" {
|
||||||
|
t.Errorf("expected prefix to be empty, got %v", o.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.contextRequestIdKey != "" {
|
||||||
|
t.Errorf("expected contextRequestIdKey to be empty, got %v", o.contextRequestIdKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.headerRequestIdKey != "" {
|
||||||
|
t.Errorf("expected headerRequestIdKey to be empty, got %v", o.headerRequestIdKey)
|
||||||
|
}
|
||||||
|
}
|
45
internal/app/api/core/middleware/logging/writer.go
Normal file
45
internal/app/api/core/middleware/logging/writer.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// writerWrapper wraps a http.ResponseWriter and tracks the number of bytes written to it.
|
||||||
|
// It also tracks the http response code passed to the WriteHeader func of
|
||||||
|
// the ResponseWriter.
|
||||||
|
type writerWrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
|
||||||
|
// StatusCode is the last http response code passed to the WriteHeader func of
|
||||||
|
// the ResponseWriter. If no such call is made, a default code of http.StatusOK
|
||||||
|
// is assumed instead.
|
||||||
|
StatusCode int
|
||||||
|
|
||||||
|
// WrittenBytes is the number of bytes successfully written by the Write or
|
||||||
|
// ReadFrom function of the ResponseWriter. ResponseWriters may also write
|
||||||
|
// data to their underlaying connection directly (e.g. headers), but those
|
||||||
|
// are not tracked. Therefor the number of Written bytes will usually match
|
||||||
|
// the size of the response body.
|
||||||
|
WrittenBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHeader wraps the WriteHeader method of the ResponseWriter and tracks the
|
||||||
|
// http response code passed to it.
|
||||||
|
func (w *writerWrapper) WriteHeader(code int) {
|
||||||
|
w.StatusCode = code
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write wraps the Write method of the ResponseWriter and tracks the number of bytes
|
||||||
|
// written to it.
|
||||||
|
func (w *writerWrapper) Write(data []byte) (int, error) {
|
||||||
|
n, err := w.ResponseWriter.Write(data)
|
||||||
|
w.WrittenBytes += int64(n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWriterWrapper returns a new writerWrapper that wraps the given http.ResponseWriter.
|
||||||
|
// It initializes the StatusCode to http.StatusOK.
|
||||||
|
func newWriterWrapper(w http.ResponseWriter) *writerWrapper {
|
||||||
|
return &writerWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user