mirror of
https://github.com/h44z/wg-portal.git
synced 2026-02-23 02:46:23 +00:00
Compare commits
5 Commits
dependabot
...
cleanup_ro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef6719817 | ||
|
|
04679dba52 | ||
|
|
e8965bb65e | ||
|
|
c00b34ac31 | ||
|
|
1fc7e352ab |
12
.github/workflows/chart.yml
vendored
12
.github/workflows/chart.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -35,16 +35,16 @@ jobs:
|
||||
# ct lint requires Python 3.x to run following packages:
|
||||
# - yamale (https://github.com/23andMe/Yamale)
|
||||
# - yamllint (https://github.com/adrienverge/yamllint)
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
|
||||
- uses: helm/chart-testing-action@v2
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --config ct.yaml
|
||||
|
||||
- uses: nolar/setup-k3d-k3s@293b8e5822a20bc0d5bcdd4826f1a665e72aba96 # v1.0.9
|
||||
- uses: nolar/setup-k3d-k3s@v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
22
.github/workflows/docker-publish.yml
vendored
22
.github/workflows/docker-publish.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
@@ -32,14 +32,14 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
wgportal/wg-portal
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
type=semver,pattern=v{{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
||||
|
||||
- name: Export binaries from images
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: binaries/wg-portal_linux*
|
||||
@@ -110,12 +110,12 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: binaries
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: 'wg-portal_linux*'
|
||||
generate_release_notes: true
|
||||
|
||||
6
.github/workflows/pages.yml
vendored
6
.github/workflows/pages.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
@@ -37,4 +37,4 @@ jobs:
|
||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||
env:
|
||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
@@ -20,7 +20,7 @@ RUN npm run build
|
||||
######
|
||||
# Build backend
|
||||
######
|
||||
FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder
|
||||
FROM --platform=${BUILDPLATFORM} golang:1.25-alpine AS builder
|
||||
# Set the working directory
|
||||
WORKDIR /build
|
||||
# Download dependencies
|
||||
@@ -50,7 +50,7 @@ COPY --from=builder /build/dist/wg-portal /
|
||||
######
|
||||
# Final image
|
||||
######
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.22
|
||||
# Install OS-level dependencies
|
||||
RUN apk add --no-cache bash curl iptables nftables openresolv wireguard-tools tzdata
|
||||
# Setup timezone
|
||||
|
||||
@@ -32,7 +32,7 @@ The configuration portal supports using a database (SQLite, MySQL, MsSQL, or Pos
|
||||
* Docker ready
|
||||
* Can be used with existing WireGuard setups
|
||||
* Support for multiple WireGuard interfaces
|
||||
* Supports multiple WireGuard backends (wgctrl, MikroTik, or pfSense)
|
||||
* Supports multiple WireGuard backends (wgctrl or MikroTik)
|
||||
* Peer Expiry Feature
|
||||
* Handles route and DNS settings like wg-quick does
|
||||
* Exposes Prometheus metrics for monitoring and alerting
|
||||
|
||||
@@ -47,7 +47,7 @@ func main() {
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
database, err := adapters.NewSqlRepository(rawDb, cfg)
|
||||
database, err := adapters.NewSqlRepository(rawDb)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
wireGuard, err := wireguard.NewControllerManager(cfg)
|
||||
@@ -84,7 +84,7 @@ func main() {
|
||||
internal.AssertNoError(err)
|
||||
userManager.StartBackgroundJobs(ctx)
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, cfg.Web.BasePath, eventBus, userManager)
|
||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
||||
internal.AssertNoError(err)
|
||||
authenticator.StartBackgroundJobs(ctx)
|
||||
|
||||
@@ -135,7 +135,6 @@ func main() {
|
||||
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
||||
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus)
|
||||
|
||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||
apiV0EndpointAuth,
|
||||
@@ -145,7 +144,6 @@ func main() {
|
||||
apiV0EndpointPeers,
|
||||
apiV0EndpointConfig,
|
||||
apiV0EndpointTest,
|
||||
apiV0EndpointWebsocket,
|
||||
)
|
||||
|
||||
// endregion API v0 (SPA frontend)
|
||||
|
||||
@@ -11,12 +11,7 @@ core:
|
||||
|
||||
web:
|
||||
external_url: http://localhost:8888
|
||||
base_path: ""
|
||||
request_logging: true
|
||||
frontend_filepath: ""
|
||||
|
||||
mail:
|
||||
templates_path: ""
|
||||
|
||||
webhook:
|
||||
url: ""
|
||||
@@ -98,16 +93,4 @@ auth:
|
||||
admin_value_regex: ^true$
|
||||
admin_group_regex: ^admin-group-name$
|
||||
registration_enabled: true
|
||||
log_user_info: true
|
||||
|
||||
backend:
|
||||
default: local
|
||||
pfsense:
|
||||
- id: pfsense1
|
||||
display_name: "Main pfSense Firewall"
|
||||
api_url: "https://pfsense.example.com" # Base URL without /api/v2 (endpoints already include it)
|
||||
api_key: "your-api-key" # Generate in pfSense under 'System' -> 'REST API' -> 'Keys'
|
||||
api_verify_tls: true
|
||||
api_timeout: 30s
|
||||
concurrency: 5
|
||||
debug: false
|
||||
log_user_info: true
|
||||
@@ -2,7 +2,7 @@ apiVersion: v2
|
||||
name: wg-portal
|
||||
description: WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
# Version is set to ensure compatibility with the chart's Ingress resource.
|
||||
kubeVersion: ">=1.19.0-0"
|
||||
kubeVersion: ">=1.19.0"
|
||||
type: application
|
||||
home: https://wgportal.org
|
||||
icon: https://wgportal.org/latest/assets/images/logo.svg
|
||||
@@ -16,7 +16,7 @@ annotations:
|
||||
# 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.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.7.2
|
||||
version: 0.7.1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# wg-portal
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
|
||||
@@ -12,7 +12,7 @@ WireGuard Configuration Portal with LDAP, OAuth, OIDC authentication
|
||||
|
||||
## Requirements
|
||||
|
||||
Kubernetes: `>=1.19.0-0`
|
||||
Kubernetes: `>=1.19.0`
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 129 KiB |
@@ -67,7 +67,8 @@ auth:
|
||||
auth:
|
||||
ldap:
|
||||
# a sample LDAP provider with user sync enabled
|
||||
- provider_name: ldap
|
||||
- id: ldap
|
||||
provider_name: Active Directory
|
||||
url: ldap://srv-ad1.company.local:389
|
||||
bind_user: ldap_wireguard@company.local
|
||||
bind_pass: super-s3cr3t-ldap
|
||||
@@ -98,7 +99,8 @@ auth:
|
||||
oidc:
|
||||
# A sample Entra ID provider with environment variable substitution.
|
||||
# Only users with an @outlook.com email address are allowed to register or login.
|
||||
- provider_name: azure
|
||||
- id: azure
|
||||
provider_name: azure
|
||||
display_name: Login with</br>Entra ID
|
||||
registration_enabled: true
|
||||
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
|
||||
@@ -111,7 +113,8 @@ auth:
|
||||
- email
|
||||
|
||||
# a sample provider where users with the attribute `wg_admin` set to `true` are considered as admins
|
||||
- provider_name: google
|
||||
- id: oidc-with-admin-attribute
|
||||
provider_name: google
|
||||
display_name: Login with</br>Google
|
||||
base_url: https://accounts.google.com
|
||||
client_id: the-client-id-1234.apps.googleusercontent.com
|
||||
@@ -133,7 +136,8 @@ auth:
|
||||
log_user_info: true
|
||||
|
||||
# a sample provider where users in the group `the-admin-group` are considered as admins
|
||||
- provider_name: google2
|
||||
- id: oidc-with-admin-group
|
||||
provider_name: google2
|
||||
display_name: Login with</br>Google2
|
||||
base_url: https://accounts.google.com
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
@@ -164,7 +168,8 @@ auth:
|
||||
oauth:
|
||||
# a sample provider where users with the attribute `this-attribute-must-be-true` set to `true` or `True`
|
||||
# are considered as admins
|
||||
- provider_name: google3
|
||||
- id: google_plain_oauth-with-admin-attribute
|
||||
provider_name: google3
|
||||
display_name: Login with</br>Google3
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
@@ -186,7 +191,8 @@ auth:
|
||||
|
||||
# a sample provider where either users with the attribute `this-attribute-must-be-true` set to `true` or
|
||||
# users in the group `admin-group-name` are considered as admins
|
||||
- provider_name: google4
|
||||
- id: google_plain_oauth_with_groups
|
||||
provider_name: google4
|
||||
display_name: Login with</br>Google4
|
||||
client_id: another-client-id-1234.apps.googleusercontent.com
|
||||
client_secret: A_CLIENT_SECRET
|
||||
|
||||
@@ -73,8 +73,6 @@ mail:
|
||||
auth_type: plain
|
||||
from: Wireguard Portal <noreply@wireguard.local>
|
||||
link_only: false
|
||||
allow_peer_email: false
|
||||
templates_path: ""
|
||||
|
||||
auth:
|
||||
oidc: []
|
||||
@@ -88,7 +86,6 @@ auth:
|
||||
web:
|
||||
listening_address: :8888
|
||||
external_url: http://localhost:8888
|
||||
base_path: ""
|
||||
site_company_name: WireGuard Portal
|
||||
site_title: WireGuard Portal
|
||||
session_identifier: wgPortalSession
|
||||
@@ -98,7 +95,6 @@ web:
|
||||
expose_host_info: false
|
||||
cert_file: ""
|
||||
key_File: ""
|
||||
frontend_filepath: ""
|
||||
|
||||
webhook:
|
||||
url: ""
|
||||
@@ -130,65 +126,51 @@ More advanced options are found in the subsequent `Advanced` section.
|
||||
|
||||
### `admin_user`
|
||||
- **Default:** `admin@wgportal.local`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_ADMIN_USER`
|
||||
- **Description:** The administrator user. This user will be created as a default admin if it does not yet exist.
|
||||
|
||||
### `admin_password`
|
||||
- **Default:** `wgportal-default`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_ADMIN_PASSWORD`
|
||||
- **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.
|
||||
|
||||
### `disable_admin_user`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_DISABLE_ADMIN_USER`
|
||||
- **Description:** If `true`, no admin user is created. This is useful if you plan to manage users exclusively through external authentication providers such as LDAP or OAuth.
|
||||
|
||||
### `admin_api_token`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_ADMIN_API_TOKEN`
|
||||
- **Description:** An API token for the admin user. If a token is provided, the REST API can be accessed using this token. If empty, the API is initially disabled for the admin user.
|
||||
|
||||
### `editable_keys`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_EDITABLE_KEYS`
|
||||
- **Description:** Allow editing of WireGuard key-pairs directly in the UI.
|
||||
|
||||
### `create_default_peer`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER`
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
|
||||
- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI).
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `create_default_peer_on_creation`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION`
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
|
||||
- **Important:** This option requires [create_default_peer](#create_default_peer) to be enabled.
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
|
||||
### `re_enable_peer_after_user_enable`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_RE_ENABLE_PEER_AFTER_USER_ENABLE`
|
||||
- **Description:** Re-enable all peers that were previously disabled if the associated user is re-enabled.
|
||||
|
||||
### `delete_peer_after_user_deleted`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_DELETE_PEER_AFTER_USER_DELETED`
|
||||
- **Description:** If a user is deleted, remove all linked peers. Otherwise, peers remain but are disabled.
|
||||
|
||||
### `self_provisioning_allowed`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_SELF_PROVISIONING_ALLOWED`
|
||||
- **Description:** Allow registered (non-admin) users to self-provision peers from their profile page.
|
||||
|
||||
### `import_existing`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_IMPORT_EXISTING`
|
||||
- **Description:** On startup, import existing WireGuard interfaces and peers into WireGuard Portal.
|
||||
|
||||
### `restore_state`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_RESTORE_STATE`
|
||||
- **Description:** Restore the WireGuard interface states (up/down) that existed before WireGuard Portal started.
|
||||
|
||||
---
|
||||
@@ -205,14 +187,11 @@ The current MikroTik backend is in **BETA** and may not support all features.
|
||||
|
||||
### `local_resolvconf_prefix`
|
||||
- **Default:** `tun.`
|
||||
- **Environment Variable:** `WG_PORTAL_BACKEND_LOCAL_RESOLVCONF_PREFIX`
|
||||
- **Description:** Interface name prefix for WireGuard interfaces on the local system which is used to configure DNS servers with *resolvconf*.
|
||||
It depends on the *resolvconf* implementation you are using, most use a prefix of `tun.`, but some have an empty prefix (e.g., systemd).
|
||||
|
||||
### `ignored_local_interfaces`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_BACKEND_IGNORED_LOCAL_INTERFACES`
|
||||
(comma-separated values)
|
||||
- **Description:** A list of interface names to exclude when enumerating local interfaces.
|
||||
This is useful if you want to prevent certain interfaces from being imported from the local system.
|
||||
|
||||
@@ -276,67 +255,54 @@ Additional or more specialized configuration options for logging and interface c
|
||||
|
||||
### `log_level`
|
||||
- **Default:** `info`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_LOG_LEVEL`
|
||||
- **Description:** The log level used by the application. Valid options are: `trace`, `debug`, `info`, `warn`, `error`.
|
||||
|
||||
### `log_pretty`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_LOG_PRETTY`
|
||||
- **Description:** If `true`, log messages are colorized and formatted for readability (pretty-print).
|
||||
|
||||
### `log_json`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_LOG_JSON`
|
||||
- **Description:** If `true`, log messages are structured in JSON format.
|
||||
|
||||
### `start_listen_port`
|
||||
- **Default:** `51820`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_START_LISTEN_PORT`
|
||||
- **Description:** The first port to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v4`
|
||||
- **Default:** `10.11.12.0/24`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_START_CIDR_V4`
|
||||
- **Description:** The initial IPv4 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `start_cidr_v6`
|
||||
- **Default:** `fdfd:d3ad:c0de:1234::0/64`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_START_CIDR_V6`
|
||||
- **Description:** The initial IPv6 subnet to use when automatically creating new WireGuard interfaces.
|
||||
|
||||
### `use_ip_v6`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_USE_IP_V6`
|
||||
- **Description:** Enable or disable IPv6 support.
|
||||
|
||||
### `config_storage_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_CONFIG_STORAGE_PATH`
|
||||
- **Description:** Path to a directory where `wg-quick` style configuration files will be stored (if you need local filesystem configs).
|
||||
|
||||
### `expiry_check_interval`
|
||||
- **Default:** `15m`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_EXPIRY_CHECK_INTERVAL`
|
||||
- **Description:** Interval after which existing peers are checked if they are expired. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `rule_prio_offset`
|
||||
- **Default:** `20000`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_RULE_PRIO_OFFSET`
|
||||
- **Description:** Offset for IP route rule priorities when configuring routing.
|
||||
|
||||
### `route_table_offset`
|
||||
- **Default:** `20000`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_ROUTE_TABLE_OFFSET`
|
||||
- **Description:** Offset for IP route table IDs when configuring routing.
|
||||
|
||||
### `api_admin_only`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_API_ADMIN_ONLY`
|
||||
- **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`
|
||||
- **Environment Variable:** `WG_PORTAL_ADVANCED_LIMIT_ADDITIONAL_USER_PEERS`
|
||||
- **Description:** Limit additional peers a normal user can create. `0` means unlimited.
|
||||
|
||||
---
|
||||
@@ -350,22 +316,18 @@ If sensitive values (like private keys) should be stored in an encrypted format,
|
||||
|
||||
### `debug`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_DATABASE_DEBUG`
|
||||
- **Description:** If `true`, logs all database statements (verbose).
|
||||
|
||||
### `slow_query_threshold`
|
||||
- **Default:** "0"
|
||||
- **Environment Variable:** `WG_PORTAL_DATABASE_SLOW_QUERY_THRESHOLD`
|
||||
- **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`
|
||||
- **Default:** `sqlite`
|
||||
- **Environment Variable:** `WG_PORTAL_DATABASE_TYPE`
|
||||
- **Description:** The database type. Valid options: `sqlite`, `mssql`, `mysql`, `postgres`.
|
||||
|
||||
### `dsn`
|
||||
- **Default:** `data/sqlite.db`
|
||||
- **Environment Variable:** `WG_PORTAL_DATABASE_DSN`
|
||||
- **Description:** The Data Source Name (DSN) for connecting to the database.
|
||||
For example:
|
||||
```text
|
||||
@@ -374,7 +336,6 @@ If sensitive values (like private keys) should be stored in an encrypted format,
|
||||
|
||||
### `encryption_passphrase`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_DATABASE_ENCRYPTION_PASSPHRASE`
|
||||
- **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.
|
||||
@@ -387,114 +348,82 @@ Controls how WireGuard Portal collects and reports usage statistics, including p
|
||||
|
||||
### `use_ping_checks`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_USE_PING_CHECKS`
|
||||
- **Description:** Enable periodic ping checks to verify that peers remain responsive.
|
||||
|
||||
### `ping_check_workers`
|
||||
- **Default:** `10`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_PING_CHECK_WORKERS`
|
||||
- **Description:** Number of parallel worker processes for ping checks.
|
||||
|
||||
### `ping_unprivileged`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_PING_UNPRIVILEGED`
|
||||
- **Description:** If `false`, ping checks run without root privileges. This is currently considered BETA.
|
||||
|
||||
### `ping_check_interval`
|
||||
- **Default:** `1m`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_PING_CHECK_INTERVAL`
|
||||
- **Description:** Interval between consecutive ping checks for all peers. Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `data_collection_interval`
|
||||
- **Default:** `1m`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_DATA_COLLECTION_INTERVAL`
|
||||
- **Description:** Interval between data collection cycles (bytes sent/received, handshake times, etc.). Format uses `s`, `m`, `h`, `d` for seconds, minutes, hours, days, see [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
|
||||
|
||||
### `collect_interface_data`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_COLLECT_INTERFACE_DATA`
|
||||
- **Description:** If `true`, collects interface-level data (bytes in/out) for monitoring and statistics.
|
||||
|
||||
### `collect_peer_data`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_COLLECT_PEER_DATA`
|
||||
- **Description:** If `true`, collects peer-level data (bytes, last handshake, endpoint, etc.).
|
||||
|
||||
### `collect_audit_data`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_COLLECT_AUDIT_DATA`
|
||||
- **Description:** If `true`, logs certain portal events (such as user logins) to the database.
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8787`
|
||||
- **Environment Variable:** `WG_PORTAL_STATISTICS_LISTENING_ADDRESS`
|
||||
- **Description:** Address and port for the integrated Prometheus metric server (e.g., `:8787` or `127.0.0.1:8787`).
|
||||
|
||||
---
|
||||
|
||||
## Mail
|
||||
|
||||
Options for configuring email notifications or sending peer configurations via email.
|
||||
By default, emails will only be sent to peers that have a valid user record linked.
|
||||
To send emails to all peers that have a valid email-address as user-identifier, set `allow_peer_email` to `true`.
|
||||
Options for configuring email notifications or sending peer configurations via email.
|
||||
|
||||
### `host`
|
||||
- **Default:** `127.0.0.1`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_HOST`
|
||||
- **Description:** Hostname or IP of the SMTP server.
|
||||
|
||||
### `port`
|
||||
- **Default:** `25`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_PORT`
|
||||
- **Description:** Port number for the SMTP server.
|
||||
|
||||
### `encryption`
|
||||
- **Default:** `none`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_ENCRYPTION`
|
||||
- **Description:** SMTP encryption type. Valid values: `none`, `tls`, `starttls`.
|
||||
|
||||
### `cert_validation`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_CERT_VALIDATION`
|
||||
- **Description:** If `true`, validate the SMTP server certificate (relevant if `encryption` = `tls`).
|
||||
|
||||
### `username`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_USERNAME`
|
||||
- **Description:** Optional SMTP username for authentication.
|
||||
|
||||
### `password`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_PASSWORD`
|
||||
- **Description:** Optional SMTP password for authentication.
|
||||
|
||||
### `auth_type`
|
||||
- **Default:** `plain`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_AUTH_TYPE`
|
||||
- **Description:** SMTP authentication type. Valid values: `plain`, `login`, `crammd5`.
|
||||
|
||||
### `from`
|
||||
- **Default:** `Wireguard Portal <noreply@wireguard.local>`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_FROM`
|
||||
- **Description:** The default "From" address when sending emails.
|
||||
|
||||
### `link_only`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_LINK_ONLY`
|
||||
- **Description:** If `true`, emails only contain a link to WireGuard Portal, rather than attaching the full configuration.
|
||||
|
||||
### `allow_peer_email`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_ALLOW_PEER_EMAIL`
|
||||
- **Description:** If `true`, and a peer has no valid user record linked, but the user-identifier of the peer is a valid email address, emails will be sent to that email address.
|
||||
If false, and the peer has no valid user record linked, emails will not be sent.
|
||||
If a peer has linked a valid user, the email address is always taken from the user record.
|
||||
|
||||
### `templates_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_MAIL_TEMPLATES_PATH`
|
||||
- **Description:** Path to the email template files that override embedded templates. Check [usage documentation](../usage/mail-templates.md) for an example.`
|
||||
|
||||
---
|
||||
|
||||
## Auth
|
||||
@@ -506,14 +435,12 @@ Some core authentication options are shared across all providers, while others a
|
||||
|
||||
### `min_password_length`
|
||||
- **Default:** `16`
|
||||
- **Environment Variable:** `WG_PORTAL_AUTH_MIN_PASSWORD_LENGTH`
|
||||
- **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`
|
||||
- **Environment Variable:** `WG_PORTAL_AUTH_HIDE_LOGIN_FORM`
|
||||
- **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).
|
||||
@@ -576,18 +503,13 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, a new user will be created in WireGuard Portal if not already present.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, OIDC user data is logged at the trace level upon login (for debugging).
|
||||
|
||||
#### `log_sensitive_info`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
||||
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
||||
|
||||
---
|
||||
|
||||
### OAuth
|
||||
@@ -654,18 +576,13 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
|
||||
- `admin_group_regex`: A regular expression to match the `user_groups` claim. Each entry in the `user_groups` claim is checked against this regex.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new users are created automatically on successful login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs user info at the trace level upon login.
|
||||
|
||||
#### `log_sensitive_info`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging).
|
||||
- **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
|
||||
|
||||
---
|
||||
|
||||
### LDAP
|
||||
@@ -682,11 +599,11 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||
- **Description:** The LDAP server URL (e.g., `ldap://srv-ad01.company.local:389`).
|
||||
|
||||
#### `start_tls`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, use STARTTLS to secure the LDAP connection.
|
||||
|
||||
#### `cert_validation`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, validate the LDAP server’s TLS certificate.
|
||||
|
||||
#### `tls_certificate_path`
|
||||
@@ -755,24 +672,20 @@ Below are the properties for each LDAP provider entry inside `auth.ldap`:
|
||||
(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))
|
||||
```
|
||||
|
||||
#### `sync_log_user_info`
|
||||
- **Default:** `false`
|
||||
- **Description:** If `true`, logs LDAP user data at the trace level during synchronization.
|
||||
|
||||
#### `disable_missing`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, any user **not** found in LDAP (during sync) is disabled in WireGuard Portal.
|
||||
|
||||
#### `auto_re_enable`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, users that where disabled because they were missing (see `disable_missing`) will be re-enabled once they are found again.
|
||||
|
||||
#### `registration_enabled`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, new user accounts are created in WireGuard Portal upon first login.
|
||||
|
||||
#### `log_user_info`
|
||||
- **Default:** `false`
|
||||
- **Default:** *(empty)*
|
||||
- **Description:** If `true`, logs LDAP user data at the trace level upon login.
|
||||
|
||||
---
|
||||
@@ -783,7 +696,6 @@ The `webauthn` section contains configuration options for WebAuthn authenticatio
|
||||
|
||||
#### `enabled`
|
||||
- **Default:** `true`
|
||||
- **Environment Variable:** `WG_PORTAL_AUTH_WEBAUTHN_ENABLED`
|
||||
- **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.
|
||||
@@ -796,76 +708,50 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
||||
|
||||
### `listening_address`
|
||||
- **Default:** `:8888`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_LISTENING_ADDRESS`
|
||||
- **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`
|
||||
- **Default:** `http://localhost:8888`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_EXTERNAL_URL`
|
||||
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||
The external URL must not contain a path component or trailing slash. If you want to serve WireGuard Portal on a subpath, use the `base_path` setting.
|
||||
- **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.
|
||||
|
||||
### `base_path`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_BASE_PATH`
|
||||
- **Description:** The base path for the web server (e.g., `/wgportal`).
|
||||
By default (meaning an empty value), the portal will be served from the root path `/`.
|
||||
|
||||
### `site_company_name`
|
||||
- **Default:** `WireGuard Portal`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_SITE_COMPANY_NAME`
|
||||
- **Description:** The company name that is shown at the bottom of the web frontend.
|
||||
|
||||
### `site_title`
|
||||
- **Default:** `WireGuard Portal`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_SITE_TITLE`
|
||||
- **Description:** The title that is shown in the web frontend.
|
||||
|
||||
### `session_identifier`
|
||||
- **Default:** `wgPortalSession`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_SESSION_IDENTIFIER`
|
||||
- **Description:** The session identifier for the web frontend.
|
||||
|
||||
### `session_secret`
|
||||
- **Default:** `very_secret`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_SESSION_SECRET`
|
||||
- **Description:** The session secret for the web frontend.
|
||||
|
||||
### `csrf_secret`
|
||||
- **Default:** `extremely_secret`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_CSRF_SECRET`
|
||||
- **Description:** The CSRF secret.
|
||||
|
||||
### `request_logging`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_REQUEST_LOGGING`
|
||||
- **Description:** Log all HTTP requests.
|
||||
|
||||
### `expose_host_info`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_EXPOSE_HOST_INFO`
|
||||
- **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`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_CERT_FILE`
|
||||
- **Description:** (Optional) Path to the TLS certificate file.
|
||||
|
||||
### `key_file`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_KEY_FILE`
|
||||
- **Description:** (Optional) Path to the TLS certificate key file.
|
||||
|
||||
### `frontend_filepath`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEB_FRONTEND_FILEPATH`
|
||||
- **Description:** Optional base directory from which the web frontend is served. Check out the [building](../getting-started/sources.md) documentation for more information on how to compile the frontend assets.
|
||||
- If the directory contains at least one file (recursively), these files are served at `/app`, overriding the embedded frontend assets.
|
||||
- If the directory is empty or does not exist on startup, the embedded frontend is copied into this directory automatically and then served.
|
||||
- If left empty, the embedded frontend is served and no files are written to disk.
|
||||
|
||||
---
|
||||
|
||||
## Webhook
|
||||
@@ -875,15 +761,12 @@ Further details can be found in the [usage documentation](../usage/webhooks.md).
|
||||
|
||||
### `url`
|
||||
- **Default:** *(empty)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEBHOOK_URL`
|
||||
- **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)*
|
||||
- **Environment Variable:** `WG_PORTAL_WEBHOOK_AUTHENTICATION`
|
||||
- **Description:** The Authorization header for the webhook endpoint. The value is send as-is in the header. For example: `Bearer <token>`.
|
||||
|
||||
### `timeout`
|
||||
- **Default:** `10s`
|
||||
- **Environment Variable:** `WG_PORTAL_WEBHOOK_TIMEOUT`
|
||||
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.
|
||||
- **Description:** The timeout for the webhook request. If the request takes longer than this, it is aborted.
|
||||
@@ -9,11 +9,6 @@ Make sure that you download the correct binary for your architecture. The availa
|
||||
- `wg-portal_linux_arm64` - Linux ARM 64-bit
|
||||
- `wg-portal_linux_arm_v7` - Linux ARM 32-bit
|
||||
|
||||
### Released versions
|
||||
|
||||
To download a specific version, replace `${WG_PORTAL_VERSION}` with the desired version (or set an environment variable).
|
||||
All official release versions can be found on the [GitHub Releases Page](https://github.com/h44z/wg-portal/releases).
|
||||
|
||||
With `curl`:
|
||||
|
||||
```shell
|
||||
@@ -32,74 +27,16 @@ with `gh cli`:
|
||||
gh release download ${WG_PORTAL_VERSION} --repo h44z/wg-portal --output wg-portal --pattern '*amd64'
|
||||
```
|
||||
|
||||
The downloaded file will be named `wg-portal` and can be moved to a directory of your choice, see [Install](#install) for more information.
|
||||
|
||||
### 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).
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
The following command can be used to install the downloaded binary (`wg-portal`) to `/opt/wg-portal/wg-portal`. It ensures that the binary is executable.
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /opt/wg-portal
|
||||
sudo install wg-portal /opt/wg-portal/
|
||||
```
|
||||
|
||||
To handle tasks such as restarting the service or configuring automatic startup, it is recommended to use a process manager like [systemd](https://systemd.io/).
|
||||
Refer to [Systemd Service Setup](#systemd-service-setup) for instructions.
|
||||
## Unreleased versions (master branch builds)
|
||||
|
||||
## Systemd Service Setup
|
||||
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).
|
||||
|
||||
> **Note:** To run WireGuard Portal as systemd service, you need to download the binary for your architecture beforehand.
|
||||
>
|
||||
> The following examples assume that you downloaded the binary to `/opt/wg-portal/wg-portal`.
|
||||
> The configuration file is expected to be located at `/opt/wg-portal/config.yml`.
|
||||
|
||||
To run WireGuard Portal as a systemd service, you can create a service unit file. The easiest way to do this is by using `systemctl edit`:
|
||||
|
||||
```shell
|
||||
sudo systemctl edit --force --full wg-portal.service
|
||||
```
|
||||
|
||||
Paste the following content into the editor and adjust the variables to your needs:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=WireGuard Portal
|
||||
ConditionPathExists=/opt/wg-portal/wg-portal
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
WorkingDirectory=/opt/wg-portal
|
||||
Environment=WG_PORTAL_CONFIG=/opt/wg-portal/config.yml
|
||||
ExecStart=/opt/wg-portal/wg-portal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Alternatively, you can create or modify the file manually in `/etc/systemd/system/wg-portal.service`.
|
||||
For systemd to pick up the changes, you need to reload the daemon:
|
||||
|
||||
```shell
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
After creating the service file, you can enable and start the service:
|
||||
|
||||
```shell
|
||||
sudo systemctl enable --now wg-portal.service
|
||||
```
|
||||
|
||||
To check status and log output, use: `sudo systemctl status wg-portal.service` or `sudo journalctl -u wg-portal.service`.
|
||||
|
||||
@@ -84,16 +84,6 @@ web:
|
||||
external_url: https://wg.domain.com
|
||||
```
|
||||
|
||||
If you want to serve the web interface on a different base-path, you can also set the `web.base_path` option:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
external_url: https://wg.domain.com
|
||||
base_path: /subpath
|
||||
```
|
||||
|
||||
The WireGuard Portal will then be available at `https://wg.domain.com/subpath`.
|
||||
|
||||
### Built-in TLS
|
||||
|
||||
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||
|
||||
@@ -443,18 +443,6 @@ definitions:
|
||||
maxLength: 64
|
||||
minLength: 32
|
||||
type: string
|
||||
AuthSources:
|
||||
description: The source of the user. This field is optional.
|
||||
example:
|
||||
- db
|
||||
items:
|
||||
enum:
|
||||
- db
|
||||
- ldap
|
||||
- oauth
|
||||
type: string
|
||||
readOnly: true
|
||||
type: array
|
||||
Department:
|
||||
description: The department of the user. This field is optional.
|
||||
example: Software Development
|
||||
@@ -515,6 +503,17 @@ definitions:
|
||||
description: The phone number of the user. This field is optional.
|
||||
example: "+1234546789"
|
||||
type: string
|
||||
ProviderName:
|
||||
description: The name of the authentication provider. This field is read-only.
|
||||
example: ""
|
||||
readOnly: true
|
||||
type: string
|
||||
Source:
|
||||
description: The source of the user. This field is optional.
|
||||
enum:
|
||||
- db
|
||||
example: db
|
||||
type: string
|
||||
required:
|
||||
- Identifier
|
||||
type: object
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
WireGuard Portal supports multiple authentication mechanisms to manage user access. This includes
|
||||
|
||||
- Local user accounts
|
||||
- LDAP authentication
|
||||
- OAuth2 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.
|
||||
|
||||
In general, each user is identified by a _unique identifier_. If the same user identifier exists across multiple authentication sources, WireGuard Portal automatically merges those accounts into a single user record.
|
||||
When a user is associated with multiple authentication sources, their information in WireGuard Portal is updated based on the most recently logged-in source. For more details, see [User Synchronization](./user-sync.md) documentation.
|
||||
|
||||
## Password Authentication
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
## OAuth2 and OIDC Authentication
|
||||
|
||||
WireGuard Portal supports OAuth2 and OIDC authentication. You can use any OAuth2 or OIDC provider that supports the authorization code flow,
|
||||
such as Google, GitHub, or Keycloak.
|
||||
|
||||
For OAuth2 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 OAuth2 authentication to WireGuard Portal, create a Client-ID and Client-Secret in your OAuth2 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 OAuth2 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 OAuth2 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 OAuth2 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.
|
||||
|
||||
|
||||
## User Synchronization
|
||||
|
||||
@@ -8,7 +8,6 @@ A global default backend determines where newly created interfaces go (unless yo
|
||||
**Supported backends:**
|
||||
- **Local** (default): Manages interfaces on the host running WireGuard Portal (Linux WireGuard via wgctrl). Use this when the portal should directly configure wg devices on the same server.
|
||||
- **MikroTik** RouterOS (_beta_): Manages interfaces and peers on MikroTik devices via the RouterOS REST API. Use this to control WG interfaces on RouterOS v7+.
|
||||
- **pfSense** (_alpha_): Manages interfaces and peers on pfSense firewalls via the pfSense REST API.
|
||||
|
||||
How backend selection works:
|
||||
- The default backend is configured at `backend.default` (_local_ or the id of a defined MikroTik backend).
|
||||
@@ -55,37 +54,4 @@ backend:
|
||||
|
||||
### Known limitations:
|
||||
- The MikroTik backend is still in beta. Some features may not work as expected.
|
||||
- Not all WireGuard Portal features are supported yet (e.g., no support for interface hooks)
|
||||
|
||||
## Configuring pfSense backends
|
||||
|
||||
> :warning: The pfSense backend is currently **alpha**. Only basic interface and peer CRUD are supported. Traffic statistics (rx/tx, last handshake) are not exposed by the pfSense REST API and will show as empty.
|
||||
|
||||
The pfSense backend talks to the pfSense REST API (pfSense Plus / CE with the REST API package installed). Point the backend at the appliance hostname without appending `/api/v2` — the portal appends `/api/v2` automatically.
|
||||
|
||||
### Prerequisites on pfSense:
|
||||
- pfSense with the REST API package enabled (`System -> API`) and WireGuard configured.
|
||||
- An API key with permissions for WireGuard endpoints. If you use a read-only key, set `core.restore_state: false` in `config.yml` to avoid write attempts at startup.
|
||||
- HTTPS recommended; set `api_verify_tls: false` only for lab/self-signed setups.
|
||||
|
||||
Example WireGuard Portal configuration:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
# default backend decides where new interfaces are created
|
||||
default: pfsense1
|
||||
|
||||
pfsense:
|
||||
- id: pfsense1 # unique id, not "local"
|
||||
display_name: Main pfSense # optional nice name
|
||||
api_url: https://pfsense.example.com # no trailing /api/v2
|
||||
api_key: your-api-key
|
||||
api_verify_tls: true
|
||||
api_timeout: 30s
|
||||
concurrency: 5
|
||||
debug: false
|
||||
```
|
||||
|
||||
### Known limitations:
|
||||
- Alpha quality: behavior and API coverage may change.
|
||||
- Statistics (rx/tx bytes, last handshake) are not available from the pfSense REST API today.
|
||||
- Not all WireGuard Portal features are supported yet (e.g., no support for interface hooks)
|
||||
@@ -14,7 +14,7 @@ WireGuard Interfaces can be categorized into three types:
|
||||
## 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) or [Authentication](authentication.md) sections for more information on securing the web UI.
|
||||
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:
|
||||
|
||||
|
||||
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.
|
||||
@@ -1,49 +0,0 @@
|
||||
WireGuard Portal sends emails when you share a configuration with a user.
|
||||
By default, the application uses embedded templates. You can fully customize these emails by pointing the Portal
|
||||
to a folder containing your own templates. If the folder is empty on startup, the default embedded templates
|
||||
are written there to get you started.
|
||||
|
||||
## Configuration
|
||||
|
||||
To enable custom templates, set the `mail.templates_path` option in the application configuration file
|
||||
or the `WG_PORTAL_MAIL_TEMPLATES_PATH` environment variable to a valid folder path.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
mail:
|
||||
# ... other mail options ...
|
||||
# Path where custom email templates (.gotpl and .gohtml) are stored.
|
||||
# If the directory is empty on startup, the default embedded templates
|
||||
# will be written there so you can modify them.
|
||||
# Leave empty to use embedded templates only.
|
||||
templates_path: "/opt/wg-portal/mail-templates"
|
||||
```
|
||||
|
||||
## Template files and names
|
||||
|
||||
The system expects the following template names. Place files with these names in your `templates_path` to override the defaults.
|
||||
You do not need to override all templates, only the ones you want to customize should be present.
|
||||
|
||||
- Text templates (`.gotpl`):
|
||||
- `mail_with_link.gotpl`
|
||||
- `mail_with_attachment.gotpl`
|
||||
- HTML templates (`.gohtml`):
|
||||
- `mail_with_link.gohtml`
|
||||
- `mail_with_attachment.gohtml`
|
||||
|
||||
Both [text](https://pkg.go.dev/text/template) and [HTML templates](https://pkg.go.dev/html/template) are standard Go
|
||||
templates and receive the following data fields, depending on the email type:
|
||||
|
||||
- Common fields:
|
||||
- `PortalUrl` (string) - external URL of the Portal
|
||||
- `PortalName` (string) - site title/company name
|
||||
- `User` (*domain.User) - the recipient user (may be partially populated when sending to a peer email)
|
||||
- Link email (`mail_with_link.*`):
|
||||
- `Link` (string) - the download link
|
||||
- Attachment email (`mail_with_attachment.*`):
|
||||
- `ConfigFileName` (string) - filename of the attached WireGuard config
|
||||
- `QrcodePngName` (string) - CID content-id of the embedded QR code image
|
||||
|
||||
Tip: You can inspect the embedded templates in the repository under [`internal/app/mail/tpl_files/`](https://github.com/h44z/wg-portal/tree/master/internal/app/mail/tpl_files) for reference.
|
||||
When the directory at `templates_path` is empty, these files are copied to your folder so you can edit them in place.
|
||||
@@ -1,12 +1,153 @@
|
||||
This section describes the security features available to administrators for hardening WireGuard Portal and protecting its data.
|
||||
|
||||
## Database Encryption
|
||||
## Authentication
|
||||
|
||||
WireGuard Portal supports multiple database backends. To reduce the risk of data exposure, sensitive information stored in the database can be encrypted.
|
||||
To enable encryption, set the [`encryption_passphrase`](../configuration/overview.md#database) in the database configuration section.
|
||||
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.
|
||||
|
||||
> :warning: Important: Once encryption is enabled, it cannot be disabled, and the passphrase cannot be changed!
|
||||
> Only new or updated records will be encrypted; existing data remains in plaintext until it’s next modified.
|
||||
|
||||
## UI and API Access
|
||||
|
||||
@@ -16,9 +157,4 @@ WireGuard Portal provides a web UI and a REST API for user interaction. It is im
|
||||
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.
|
||||
|
||||
### Secure Authentication
|
||||
To prevent unauthorized access, WireGuard Portal supports integrating with secure authentication providers such as LDAP, OAuth2, or Passkeys, see [Authentication](./authentication.md) for more details.
|
||||
When possible, use centralized authentication and enforce multi-factor authentication (MFA) at the provider level for enhanced account security.
|
||||
For local accounts, administrators should enforce strong password requirements.
|
||||
A detailed explanation is available in the [Reverse Proxy](../getting-started/reverse-proxy.md) section.
|
||||
@@ -1,46 +0,0 @@
|
||||
For all external authentication providers (LDAP, OIDC, OAuth2), WireGuard Portal can automatically create a local user record upon the user's first successful login.
|
||||
This behavior is controlled by the `registration_enabled` setting in each authentication provider's configuration.
|
||||
|
||||
User information from external authentication sources is merged into the corresponding local WireGuard Portal user record whenever the user logs in.
|
||||
Additionally, WireGuard Portal supports periodic synchronization of user data from an LDAP directory.
|
||||
|
||||
To prevent overwriting local changes, WireGuard Portal allows you to set a per-user flag that disables synchronization of external attributes.
|
||||
When this flag is set, the user in WireGuard Portal will not be updated automatically during log-ins or LDAP synchronization.
|
||||
|
||||
### LDAP Synchronization
|
||||
|
||||
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. Details on the log-in process can be found in the [LDAP Authentication](./authentication.md#ldap-authentication) section.
|
||||
|
||||
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.
|
||||
|
||||
#### Synchronization Parameters
|
||||
|
||||
To enable the LDAP sycnhronization 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 were disabled by the synchronization process. Manually disabled users will not be re-enabled.
|
||||
@@ -68,32 +68,26 @@ All payload models are encoded as JSON objects. Fields with empty values might b
|
||||
|
||||
#### User Payload (entity: `user`)
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|----------------|---------------|-----------------------------------|
|
||||
| CreatedBy | string | Creator identifier |
|
||||
| UpdatedBy | string | Last updater identifier |
|
||||
| CreatedAt | time.Time | Time of creation |
|
||||
| UpdatedAt | time.Time | Time of last update |
|
||||
| Identifier | string | Unique user identifier |
|
||||
| Email | string | User email |
|
||||
| AuthSources | []AuthSource | Authentication sources |
|
||||
| IsAdmin | bool | Whether user has admin privileges |
|
||||
| Firstname | string | User's first name (optional) |
|
||||
| Lastname | string | User's last name (optional) |
|
||||
| Phone | string | Contact phone number (optional) |
|
||||
| Department | string | User's department (optional) |
|
||||
| Notes | string | Additional notes (optional) |
|
||||
| Disabled | *time.Time | When user was disabled |
|
||||
| DisabledReason | string | Reason for deactivation |
|
||||
| Locked | *time.Time | When user account was locked |
|
||||
| LockedReason | string | Reason for being locked |
|
||||
|
||||
`AuthSource`:
|
||||
|
||||
| JSON Field | Type | Description |
|
||||
|--------------|---------------|-----------------------------------------------------|
|
||||
| Source | string | The authentication source (e.g. LDAP, OAuth, or DB) |
|
||||
| ProviderName | string | The identifier of the authentication provider |
|
||||
| JSON Field | Type | Description |
|
||||
|----------------|-------------|-----------------------------------|
|
||||
| CreatedBy | string | Creator identifier |
|
||||
| UpdatedBy | string | Last updater identifier |
|
||||
| CreatedAt | time.Time | Time of creation |
|
||||
| UpdatedAt | time.Time | Time of last update |
|
||||
| Identifier | string | Unique user identifier |
|
||||
| Email | string | User email |
|
||||
| Source | string | Authentication source |
|
||||
| ProviderName | string | Name of auth provider |
|
||||
| IsAdmin | bool | Whether user has admin privileges |
|
||||
| Firstname | string | User's first name (optional) |
|
||||
| Lastname | string | User's last name (optional) |
|
||||
| Phone | string | Contact phone number (optional) |
|
||||
| Department | string | User's department (optional) |
|
||||
| Notes | string | Additional notes (optional) |
|
||||
| Disabled | *time.Time | When user was disabled |
|
||||
| DisabledReason | string | Reason for deactivation |
|
||||
| Locked | *time.Time | When user account was locked |
|
||||
| LockedReason | string | Reason for being locked |
|
||||
|
||||
|
||||
#### Peer Payload (entity: `peer`)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
.tx-hero__image {
|
||||
max-width: 1000px;
|
||||
min-width: 0;
|
||||
min-width: 600px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
.secondary-section .g .section .component-wrapper .responsive-grid .card {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
background-color: #fff none repeat scroll 0% 0%;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -363,6 +363,7 @@
|
||||
<h1>A beautiful and simple UI to manage your WireGuard peers and interfaces</h1>
|
||||
<p>WireGuard Portal is an open source web-based user interface that makes it easy to setup and manage
|
||||
WireGuard VPN connections. It's built on top of WireGuard's official <span class="em">wgctrl</span> library.</p>
|
||||
</p>
|
||||
<a
|
||||
href="documentation/overview/"
|
||||
title="Get Started"
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<script>
|
||||
// global config, will be overridden by backend if available
|
||||
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
||||
let WGPORTAL_BASE_PATH="";
|
||||
let WGPORTAL_VERSION="unknown";
|
||||
let WGPORTAL_SITE_TITLE="WireGuard Portal";
|
||||
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
|
||||
|
||||
514
frontend/package-lock.json
generated
514
frontend/package-lock.json
generated
@@ -8,30 +8,29 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@fontsource/nunito-sans": "^5.2.7",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@kyvg/vue3-notification": "^3.4.2",
|
||||
"@fontsource/nunito-sans": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootswatch": "^5.3.8",
|
||||
"cidr-tools": "^11.0.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"ip-address": "^10.1.0",
|
||||
"is-cidr": "^6.0.1",
|
||||
"bootstrap": "^5.3.7",
|
||||
"bootswatch": "^5.3.7",
|
||||
"flag-icons": "^7.3.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"is-cidr": "^5.1.1",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia": "^3.0.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"vite": "^7.2.7"
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vite": "^6.3.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -44,21 +43,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
"@babel/types": "^7.28.4"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -68,13 +67,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -539,22 +538,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz",
|
||||
"integrity": "sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.12.tgz",
|
||||
"integrity": "sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
"@intlify/message-compiler": "11.1.12",
|
||||
"@intlify/shared": "11.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -564,12 +563,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.12.tgz",
|
||||
"integrity": "sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/shared": "11.1.12",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -580,9 +579,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.12.tgz",
|
||||
"integrity": "sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -598,9 +597,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@kyvg/vue3-notification": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@kyvg/vue3-notification/-/vue3-notification-3.4.2.tgz",
|
||||
"integrity": "sha512-CZ2zOdXsbGCtWbdqMgbusKtZTkMT+dYpw9bmAitsdSNHT0knh4njD8X95JIyTMWvNVjhDkFedbkNiZLcPqttwQ==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@kyvg/vue3-notification/-/vue3-notification-3.4.1.tgz",
|
||||
"integrity": "sha512-WhTWCbF36JHLJR5UdKmJF7KXGOGVy4tLeaJuKTHZhwttZWnbF9w1/c2d32tvCSwY9CdeX/n9uoaKWLMKK3vOyg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
@@ -921,18 +920,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.50",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
|
||||
"integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
|
||||
@@ -1242,9 +1235,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@simplewebauthn/browser": {
|
||||
"version": "13.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
|
||||
"integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==",
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.0.tgz",
|
||||
"integrity": "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
@@ -1255,19 +1248,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
|
||||
"integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==",
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-beta.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
@@ -1286,53 +1276,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
||||
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.25",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/shared": "3.5.22",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
|
||||
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"magic-string": "^0.30.19",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
|
||||
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
||||
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -1369,53 +1359,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.25.tgz",
|
||||
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
|
||||
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
|
||||
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.25",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@vue/shared": "3.5.25",
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
|
||||
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz",
|
||||
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
|
||||
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/birpc": {
|
||||
@@ -1491,27 +1481,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cidr-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-2Apfc6qH9uwF3QHmlYBA8ExB9VHq+1/Doj9sEMY55TVBcpQ3y/+gmMpcNIBBtfb5k54Vphmta+1IxjMqPlWWAA==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-4.1.3.tgz",
|
||||
"integrity": "sha512-86M1y3ZeQvpZkZejQCcS+IaSWjlDUC+ORP0peScQ4uEUFCZ8bEQVz7NlJHqysoUb6w3zCjx4Mq/8/2RHhMwHYw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"ip-regex": "5.0.0"
|
||||
"ip-regex": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cidr-tools": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cidr-tools/-/cidr-tools-11.0.3.tgz",
|
||||
"integrity": "sha512-7p0rp7B2P+nZfBkJlrQzUMDyUHeYK2h/XCJY80VUl1v5oxwLxQjZMy39BXVOXugwAX67l0oJ/QQ6OhANgUtUbw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"ip-bigint": "^8.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
@@ -1564,9 +1542,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
@@ -1720,23 +1698,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-bigint": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ip-bigint/-/ip-bigint-8.2.2.tgz",
|
||||
"integrity": "sha512-wPoOpHigOtoY29UCFA0L82cJVFcT7M+TsrgipUVpFw7HV9LpLEuNXCymt3623jzHPlIZzFaCyaVf9VACssFYew==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz",
|
||||
@@ -1750,15 +1719,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-cidr": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-6.0.1.tgz",
|
||||
"integrity": "sha512-JIJlvXodfsoWFAvvjB7Elqu8qQcys2SZjkIJCLdk4XherUqZ6+zH7WIpXkp4B3ZxMH0Fz7zIsZwyvs6JfM0csw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-5.1.1.tgz",
|
||||
"integrity": "sha512-AwzRMjtJNTPOgm7xuYZ71715z99t+4yRnSnSzgK5err5+heYi4zMuvmpUadaJ28+KCXCQo8CjUrKQZRWSPmqTQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"cidr-regex": "5.0.1"
|
||||
"cidr-regex": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
@@ -1838,9 +1807,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -1920,19 +1889,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
|
||||
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
"@vue/devtools-api": "^7.7.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
"vue": "^3.5.11"
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -2051,9 +2020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.3.tgz",
|
||||
"integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2073,11 +2042,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.3.tgz",
|
||||
"integrity": "sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.93.2.tgz",
|
||||
"integrity": "sha512-FvQdkn2dZ8DGiLgi0Uf4zsj7r/BsiLImNa5QJ10eZalY6NfZyjrmWGFcuCN5jNwlDlXFJnftauv+UtvBKLvepQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.5.0",
|
||||
"buffer-builder": "^0.2.0",
|
||||
@@ -2095,30 +2065,30 @@
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded-all-unknown": "1.93.3",
|
||||
"sass-embedded-android-arm": "1.93.3",
|
||||
"sass-embedded-android-arm64": "1.93.3",
|
||||
"sass-embedded-android-riscv64": "1.93.3",
|
||||
"sass-embedded-android-x64": "1.93.3",
|
||||
"sass-embedded-darwin-arm64": "1.93.3",
|
||||
"sass-embedded-darwin-x64": "1.93.3",
|
||||
"sass-embedded-linux-arm": "1.93.3",
|
||||
"sass-embedded-linux-arm64": "1.93.3",
|
||||
"sass-embedded-linux-musl-arm": "1.93.3",
|
||||
"sass-embedded-linux-musl-arm64": "1.93.3",
|
||||
"sass-embedded-linux-musl-riscv64": "1.93.3",
|
||||
"sass-embedded-linux-musl-x64": "1.93.3",
|
||||
"sass-embedded-linux-riscv64": "1.93.3",
|
||||
"sass-embedded-linux-x64": "1.93.3",
|
||||
"sass-embedded-unknown-all": "1.93.3",
|
||||
"sass-embedded-win32-arm64": "1.93.3",
|
||||
"sass-embedded-win32-x64": "1.93.3"
|
||||
"sass-embedded-all-unknown": "1.93.2",
|
||||
"sass-embedded-android-arm": "1.93.2",
|
||||
"sass-embedded-android-arm64": "1.93.2",
|
||||
"sass-embedded-android-riscv64": "1.93.2",
|
||||
"sass-embedded-android-x64": "1.93.2",
|
||||
"sass-embedded-darwin-arm64": "1.93.2",
|
||||
"sass-embedded-darwin-x64": "1.93.2",
|
||||
"sass-embedded-linux-arm": "1.93.2",
|
||||
"sass-embedded-linux-arm64": "1.93.2",
|
||||
"sass-embedded-linux-musl-arm": "1.93.2",
|
||||
"sass-embedded-linux-musl-arm64": "1.93.2",
|
||||
"sass-embedded-linux-musl-riscv64": "1.93.2",
|
||||
"sass-embedded-linux-musl-x64": "1.93.2",
|
||||
"sass-embedded-linux-riscv64": "1.93.2",
|
||||
"sass-embedded-linux-x64": "1.93.2",
|
||||
"sass-embedded-unknown-all": "1.93.2",
|
||||
"sass-embedded-win32-arm64": "1.93.2",
|
||||
"sass-embedded-win32-x64": "1.93.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-all-unknown": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.3.tgz",
|
||||
"integrity": "sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.93.2.tgz",
|
||||
"integrity": "sha512-GdEuPXIzmhRS5J7UKAwEvtk8YyHQuFZRcpnEnkA3rwRUI27kwjyXkNeIj38XjUQ3DzrfMe8HcKFaqWGHvblS7Q==",
|
||||
"cpu": [
|
||||
"!arm",
|
||||
"!arm64",
|
||||
@@ -2129,13 +2099,13 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"sass": "1.93.3"
|
||||
"sass": "1.93.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-arm": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.3.tgz",
|
||||
"integrity": "sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.93.2.tgz",
|
||||
"integrity": "sha512-I8bpO8meZNo5FvFx5FIiE7DGPVOYft0WjuwcCCdeJ6duwfkl6tZdatex1GrSigvTsuz9L0m4ngDcX/Tj/8yMow==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2150,9 +2120,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-arm64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.3.tgz",
|
||||
"integrity": "sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.93.2.tgz",
|
||||
"integrity": "sha512-346f4iVGAPGcNP6V6IOOFkN5qnArAoXNTPr5eA/rmNpeGwomdb7kJyQ717r9rbJXxOG8OAAUado6J0qLsjnjXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2167,9 +2137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-riscv64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.3.tgz",
|
||||
"integrity": "sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.93.2.tgz",
|
||||
"integrity": "sha512-hSMW1s4yJf5guT9mrdkumluqrwh7BjbZ4MbBW9tmi1DRDdlw1Wh9Oy1HnnmOG8x9XcI1qkojtPL6LUuEJmsiDg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2184,9 +2154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-android-x64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.3.tgz",
|
||||
"integrity": "sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.93.2.tgz",
|
||||
"integrity": "sha512-JqktiHZduvn+ldGBosE40ALgQ//tGCVNAObgcQ6UIZznEJbsHegqStqhRo8UW3x2cgOO2XYJcrInH6cc7wdKbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2201,9 +2171,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-darwin-arm64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.3.tgz",
|
||||
"integrity": "sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.93.2.tgz",
|
||||
"integrity": "sha512-qI1X16qKNeBJp+M/5BNW7v/JHCDYWr1/mdoJ7+UMHmP0b5AVudIZtimtK0hnjrLnBECURifd6IkulybR+h+4UA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2218,9 +2188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-darwin-x64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.3.tgz",
|
||||
"integrity": "sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.93.2.tgz",
|
||||
"integrity": "sha512-4KeAvlkQ0m0enKUnDGQJZwpovYw99iiMb8CTZRSsQm8Eh7halbJZVmx67f4heFY/zISgVOCcxNg19GrM5NTwtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2235,9 +2205,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-arm": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.3.tgz",
|
||||
"integrity": "sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.93.2.tgz",
|
||||
"integrity": "sha512-N3+D/ToHtzwLDO+lSH05Wo6/KRxFBPnbjVHASOlHzqJnK+g5cqex7IFAp6ozzlRStySk61Rp6d+YGrqZ6/P0PA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2252,9 +2222,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-arm64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.3.tgz",
|
||||
"integrity": "sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.93.2.tgz",
|
||||
"integrity": "sha512-9ftX6nd5CsShJqJ2WRg+ptaYvUW+spqZfJ88FbcKQBNFQm6L87luj3UI1rB6cP5EWrLwHA754OKxRJyzWiaN6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2269,9 +2239,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-arm": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.3.tgz",
|
||||
"integrity": "sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.93.2.tgz",
|
||||
"integrity": "sha512-XBTvx66yRenvEsp3VaJCb3HQSyqCsUh7R+pbxcN5TuzueybZi0LXvn9zneksdXcmjACMlMpIVXi6LyHPQkYc8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2286,9 +2256,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-arm64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.3.tgz",
|
||||
"integrity": "sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.93.2.tgz",
|
||||
"integrity": "sha512-+3EHuDPkMiAX5kytsjEC1bKZCawB9J6pm2eBIzzLMPWbf5xdx++vO1DpT7hD4bm4ZGn0eVHgSOKIfP6CVz6tVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2303,9 +2273,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-riscv64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.3.tgz",
|
||||
"integrity": "sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.93.2.tgz",
|
||||
"integrity": "sha512-0sB5kmVZDKTYzmCSlTUnjh6mzOhzmQiW/NNI5g8JS4JiHw2sDNTvt1dsFTuqFkUHyEOY3ESTsfHHBQV8Ip4bEA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2320,9 +2290,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-musl-x64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.3.tgz",
|
||||
"integrity": "sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.93.2.tgz",
|
||||
"integrity": "sha512-t3ejQ+1LEVuHy7JHBI2tWHhoMfhedUNDjGJR2FKaLgrtJntGnyD1RyX0xb3nuqL/UXiEAtmTmZY+Uh3SLUe1Hg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2337,9 +2307,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-riscv64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.3.tgz",
|
||||
"integrity": "sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.93.2.tgz",
|
||||
"integrity": "sha512-e7AndEwAbFtXaLy6on4BfNGTr3wtGZQmypUgYpSNVcYDO+CWxatKVY4cxbehMPhxG9g5ru+eaMfynvhZt7fLaA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2354,9 +2324,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-linux-x64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.3.tgz",
|
||||
"integrity": "sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.93.2.tgz",
|
||||
"integrity": "sha512-U3EIUZQL11DU0xDDHXexd4PYPHQaSQa2hzc4EzmhHqrAj+TyfYO94htjWOd+DdTPtSwmLp+9cTWwPZBODzC96w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2371,9 +2341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-unknown-all": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.3.tgz",
|
||||
"integrity": "sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.93.2.tgz",
|
||||
"integrity": "sha512-7VnaOmyewcXohiuoFagJ3SK5ddP9yXpU0rzz+pZQmS1/+5O6vzyFCUoEt3HDRaLctH4GT3nUGoK1jg0ae62IfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2384,13 +2354,13 @@
|
||||
"!win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"sass": "1.93.3"
|
||||
"sass": "1.93.2"
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-win32-arm64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.3.tgz",
|
||||
"integrity": "sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.93.2.tgz",
|
||||
"integrity": "sha512-Y90DZDbQvtv4Bt0GTXKlcT9pn4pz8AObEjFF8eyul+/boXwyptPZ/A1EyziAeNaIEIfxyy87z78PUgCeGHsx3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2405,9 +2375,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass-embedded-win32-x64": {
|
||||
"version": "1.93.3",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.3.tgz",
|
||||
"integrity": "sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==",
|
||||
"version": "1.93.2",
|
||||
"resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.93.2.tgz",
|
||||
"integrity": "sha512-BbSucRP6PVRZGIwlEBkp+6VQl2GWdkWFMN+9EuOTPrLxCJZoq+yhzmbjspd3PeM8+7WJ7AdFu/uRYdO8tor1iQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2569,6 +2539,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2605,24 +2576,25 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
|
||||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
@@ -2631,14 +2603,14 @@
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
@@ -2703,6 +2675,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -2711,16 +2684,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
|
||||
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.25",
|
||||
"@vue/compiler-sfc": "3.5.25",
|
||||
"@vue/runtime-dom": "3.5.25",
|
||||
"@vue/server-renderer": "3.5.25",
|
||||
"@vue/shared": "3.5.25"
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/runtime-dom": "3.5.22",
|
||||
"@vue/server-renderer": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -2732,13 +2706,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"version": "11.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.12.tgz",
|
||||
"integrity": "sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@intlify/core-base": "11.1.12",
|
||||
"@intlify/shared": "11.1.12",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2763,9 +2737,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
||||
"integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.4"
|
||||
@@ -2774,7 +2748,7 @@
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||
|
||||
@@ -8,29 +8,28 @@
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/nunito-sans": "^5.2.7",
|
||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||
"@kyvg/vue3-notification": "^3.4.2",
|
||||
"@fontsource/nunito-sans": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@kyvg/vue3-notification": "^3.4.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/browser": "^13.1.0",
|
||||
"@vojtechlanka/vue-tags-input": "^3.1.1",
|
||||
"bootstrap": "^5.3.8",
|
||||
"bootswatch": "^5.3.8",
|
||||
"cidr-tools": "^11.0.3",
|
||||
"flag-icons": "^7.5.0",
|
||||
"ip-address": "^10.1.0",
|
||||
"is-cidr": "^6.0.1",
|
||||
"bootstrap": "^5.3.7",
|
||||
"bootswatch": "^5.3.7",
|
||||
"flag-icons": "^7.3.2",
|
||||
"ip-address": "^10.0.1",
|
||||
"is-cidr": "^5.1.1",
|
||||
"is-ip": "^5.0.1",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia": "^3.0.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"vue": "^3.5.25",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-prism-component": "github:h44z/vue-prism-component",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"vite": "^7.2.7"
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"vite": "^6.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ const languageFlag = computed(() => {
|
||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
||||
const wgVersion = ref(WGPORTAL_VERSION);
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
const webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
let displayName = "Unknown";
|
||||
@@ -114,7 +113,7 @@ const userDisplayName = computed(() => {
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<RouterLink class="navbar-brand" :to="{ name: 'home' }"><img :alt="companyName" :src="webBasePath + '/img/header-logo.png'" /></RouterLink>
|
||||
<a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
|
||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
@@ -134,9 +133,6 @@ const userDisplayName = computed(() => {
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'key-generator' }" class="nav-link">{{ $t('menu.keygen') }}</RouterLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<RouterLink :to="{ name: 'ip-calculator' }" class="nav-link">{{ $t('menu.calculator') }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-nav d-flex justify-content-end">
|
||||
|
||||
@@ -83,7 +83,6 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||
formData.value.Mode = interfaces.Prepared.Mode
|
||||
formData.value.CreateDefaultPeer = interfaces.Prepared.CreateDefaultPeer
|
||||
formData.value.Backend = interfaces.Prepared.Backend
|
||||
|
||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||
@@ -123,7 +122,6 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = selectedInterface.value.Identifier
|
||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||
formData.value.Mode = selectedInterface.value.Mode
|
||||
formData.value.CreateDefaultPeer = selectedInterface.value.CreateDefaultPeer
|
||||
formData.value.Backend = selectedInterface.value.Backend
|
||||
|
||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||
@@ -489,10 +487,6 @@ async function del() {
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Mode==='server' && settings.Setting('CreateDefaultPeer')">
|
||||
<input v-model="formData.CreateDefaultPeer" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.create-default-peer.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Backend==='local'">
|
||||
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>
|
||||
|
||||
@@ -42,7 +42,7 @@ const passwordWeak = computed(() => {
|
||||
})
|
||||
|
||||
const formValid = computed(() => {
|
||||
if (!formData.value.AuthSources.some(s => s === 'db')) {
|
||||
if (formData.value.Source !== 'db') {
|
||||
return true // nothing to validate
|
||||
}
|
||||
if (props.userId !== '#NEW#' && passwordWeak.value) {
|
||||
@@ -70,7 +70,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
} else { // fill existing userdata
|
||||
formData.value.Identifier = selectedUser.value.Identifier
|
||||
formData.value.Email = selectedUser.value.Email
|
||||
formData.value.AuthSources = selectedUser.value.AuthSources
|
||||
formData.value.Source = selectedUser.value.Source
|
||||
formData.value.IsAdmin = selectedUser.value.IsAdmin
|
||||
formData.value.Firstname = selectedUser.value.Firstname
|
||||
formData.value.Lastname = selectedUser.value.Lastname
|
||||
@@ -80,7 +80,6 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Password = ""
|
||||
formData.value.Disabled = selectedUser.value.Disabled
|
||||
formData.value.Locked = selectedUser.value.Locked
|
||||
formData.value.PersistLocalChanges = selectedUser.value.PersistLocalChanges
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +133,7 @@ async function del() {
|
||||
<template>
|
||||
<Modal :title="title" :visible="visible" @close="close">
|
||||
<template #default>
|
||||
<fieldset>
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-general') }}</legend>
|
||||
<div v-if="props.userId==='#NEW#'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.identifier.label') }}</label>
|
||||
@@ -142,22 +141,16 @@ async function del() {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.source.label') }}</label>
|
||||
<input v-model="formData.AuthSources" class="form-control" disabled="disabled" :placeholder="$t('modals.user-edit.source.placeholder')" type="text">
|
||||
<input v-model="formData.Source" class="form-control" disabled="disabled" :placeholder="$t('modals.user-edit.source.placeholder')" type="text">
|
||||
</div>
|
||||
<div class="form-group" v-if="formData.AuthSources.some(s => s ==='db')">
|
||||
<div v-if="formData.Source==='db'" class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.password.label') }}</label>
|
||||
<input v-model="formData.Password" aria-describedby="passwordHelp" class="form-control" :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>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="formData.AuthSources.some(s => s !=='db') && !formData.PersistLocalChanges">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-personal') }}</legend>
|
||||
<div class="alert alert-warning mt-3">
|
||||
{{ $t('modals.user-edit.sync-warning') }}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset v-if="!formData.AuthSources.some(s => s !=='db') || formData.PersistLocalChanges">
|
||||
<fieldset v-if="formData.Source==='db'">
|
||||
<legend class="mt-4">{{ $t('modals.user-edit.header-personal') }}</legend>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('modals.user-edit.email.label') }}</label>
|
||||
@@ -201,14 +194,10 @@ async function del() {
|
||||
<input v-model="formData.Locked" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.locked.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="!formData.AuthSources.some(s => s !=='db') || formData.PersistLocalChanges">
|
||||
<div class="form-check form-switch" v-if="formData.Source==='db'">
|
||||
<input v-model="formData.IsAdmin" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.user-edit.admin.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.AuthSources.some(s => s !=='db')">
|
||||
<input v-model="formData.PersistLocalChanges" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" >{{ $t('modals.user-edit.persist-local-changes.label') }}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function base64_url_encode(input) {
|
||||
let output = btoa(input)
|
||||
output = output.replaceAll('+', '.')
|
||||
output = output.replaceAll('/', '_')
|
||||
output = output.replaceAll('=', '-')
|
||||
output = output.replace('+', '.')
|
||||
output = output.replace('/', '_')
|
||||
output = output.replace('=', '-')
|
||||
return output
|
||||
}
|
||||
@@ -4,7 +4,6 @@ export function freshInterface() {
|
||||
Disabled: false,
|
||||
DisplayName: "",
|
||||
Identifier: "",
|
||||
CreateDefaultPeer: false,
|
||||
Mode: "server",
|
||||
Backend: "local",
|
||||
|
||||
@@ -137,7 +136,7 @@ export function freshUser() {
|
||||
Identifier: "",
|
||||
|
||||
Email: "",
|
||||
AuthSources: ["db"],
|
||||
Source: "db",
|
||||
IsAdmin: false,
|
||||
|
||||
Firstname: "",
|
||||
@@ -155,8 +154,6 @@ export function freshUser() {
|
||||
|
||||
ApiEnabled: false,
|
||||
|
||||
PersistLocalChanges: false,
|
||||
|
||||
PeerCount: 0,
|
||||
|
||||
// Internal values
|
||||
|
||||
@@ -4,12 +4,12 @@ export function ipToBigInt(ip) {
|
||||
// Check if it's an IPv4 address
|
||||
if (ip.includes(".")) {
|
||||
const addr = new Address4(ip)
|
||||
return addr.bigInt()
|
||||
return addr.bigInteger()
|
||||
}
|
||||
|
||||
// Otherwise, assume it's an IPv6 address
|
||||
const addr = new Address6(ip)
|
||||
return addr.bigInt()
|
||||
return addr.bigInteger()
|
||||
}
|
||||
|
||||
export function humanFileSize(size) {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { peerStore } from '@/stores/peers';
|
||||
import { interfaceStore } from '@/stores/interfaces';
|
||||
import { authStore } from '@/stores/auth';
|
||||
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let failureCount = 0;
|
||||
|
||||
export const websocketWrapper = {
|
||||
connect() {
|
||||
if (socket) {
|
||||
console.log('WebSocket already connected, re-using existing connection.');
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = WGPORTAL_BACKEND_BASE_URL.startsWith('https://') ? 'wss://' : 'ws://';
|
||||
const baseUrl = WGPORTAL_BACKEND_BASE_URL.replace(/^https?:\/\//, '');
|
||||
const url = `${protocol}${baseUrl}/ws`;
|
||||
|
||||
socket = new WebSocket(url);
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
failureCount = 0;
|
||||
if (reconnectTimer) {
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
failureCount++;
|
||||
socket = null;
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
failureCount++;
|
||||
socket.close();
|
||||
socket = null;
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
switch (message.type) {
|
||||
case 'peer_stats':
|
||||
peerStore().updatePeerTrafficStats(message.data);
|
||||
break;
|
||||
case 'interface_stats':
|
||||
interfaceStore().updateInterfaceTrafficStats(message.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
if (reconnectTimer) {
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
failureCount = 0;
|
||||
}
|
||||
},
|
||||
|
||||
scheduleReconnect() {
|
||||
if (reconnectTimer) return;
|
||||
if (!authStore().IsAuthenticated) return; // Don't reconnect if not logged in
|
||||
|
||||
reconnectTimer = setInterval(() => {
|
||||
if (failureCount > 2) {
|
||||
console.log('WebSocket connection unavailable, giving up.');
|
||||
clearInterval(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Attempting to reconnect WebSocket...');
|
||||
this.connect();
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
@@ -42,8 +42,7 @@
|
||||
"audit": "Event Protokoll",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"keygen": "Schlüsselgenerator",
|
||||
"calculator": "IP-Rechner"
|
||||
"keygen": "Schlüsselgenerator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -129,11 +128,6 @@
|
||||
"button-add-peers": "Mehrere Peers hinzufügen",
|
||||
"button-show-peer": "Peer anzeigen",
|
||||
"button-edit-peer": "Peer bearbeiten",
|
||||
"button-bulk-delete": "Ausgewählte Peers löschen",
|
||||
"button-bulk-enable": "Ausgewählte Peers aktivieren",
|
||||
"button-bulk-disable": "Ausgewählte Peers deaktivieren",
|
||||
"confirm-bulk-delete": "Sind Sie sicher, dass Sie {count} Peers löschen möchten?",
|
||||
"confirm-bulk-disable": "Sind Sie sicher, dass Sie {count} Peers deaktivieren möchten?",
|
||||
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
||||
"peer-expiring": "Peer läuft ab am",
|
||||
"peer-connected": "Verbunden",
|
||||
@@ -147,7 +141,7 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"lastname": "Nachname",
|
||||
"sources": "Quellen",
|
||||
"source": "Quelle",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
@@ -158,14 +152,6 @@
|
||||
"button-add-user": "Benutzer hinzufügen",
|
||||
"button-show-user": "Benutzer anzeigen",
|
||||
"button-edit-user": "Benutzer bearbeiten",
|
||||
"button-bulk-delete": "Ausgewählte Benutzer löschen",
|
||||
"button-bulk-enable": "Ausgewählte Benutzer aktivieren",
|
||||
"button-bulk-disable": "Ausgewählte Benutzer deaktivieren",
|
||||
"button-bulk-lock": "Ausgewählte Benutzer sperren",
|
||||
"button-bulk-unlock": "Ausgewählte Benutzer entsperren",
|
||||
"confirm-bulk-delete": "Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?",
|
||||
"confirm-bulk-disable": "Sind Sie sicher, dass Sie {count} Benutzer deaktivieren möchten?",
|
||||
"confirm-bulk-lock": "Sind Sie sicher, dass Sie {count} Benutzer sperren möchten?",
|
||||
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
||||
"user-locked": "Konto ist gesperrt, Grund:",
|
||||
"admin": "Benutzer hat Administratorrechte",
|
||||
@@ -235,16 +221,6 @@
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"headline": "Passwort-Einstellungen",
|
||||
"abstract": "Hier können Sie Ihr Passwort ändern.",
|
||||
"current-label": "Aktuelles Passwort",
|
||||
"new-label": "Neues Passwort",
|
||||
"new-confirm-label": "Neues Passwort bestätigen",
|
||||
"change-button-text": "Passwort ändern",
|
||||
"invalid-confirm-label": "Passwörter stimmen nicht überein",
|
||||
"weak-label": "Passwort ist zu schwach"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
@@ -283,26 +259,6 @@
|
||||
"placeholder": "Der geteilte Schlüssel"
|
||||
}
|
||||
},
|
||||
"calculator": {
|
||||
"headline": "WireGuard IP-Rechner",
|
||||
"abstract": "Erzeuge erlaubte IPs für WireGuard. Die IP-Subnetze werden lokal in Ihrem Browser generiert und niemals an den Server gesendet.",
|
||||
"headline-allowed-ip": "Neue erlaubte IPs",
|
||||
"button-exclude-private": "Private IP-Bereiche ausschließen",
|
||||
"allowed-ip": {
|
||||
"label": "Erlaubte IPs",
|
||||
"placeholder": "0.0.0.0/0, ::/0",
|
||||
"empty": "Wert darf nicht leer sein"
|
||||
},
|
||||
"dissallowed-ip": {
|
||||
"label": "Nicht erlaubte IPs",
|
||||
"placeholder": "10.0.0.0/8, 192.168.0.0/16",
|
||||
"invalid": "Ungültige Adresse: {addr}"
|
||||
},
|
||||
"new-allowed-ip": {
|
||||
"label": "Erlaubte IPs",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "Benutzerkonto:",
|
||||
@@ -378,11 +334,7 @@
|
||||
},
|
||||
"admin": {
|
||||
"label": "Ist Administrator"
|
||||
},
|
||||
"persist-local-changes": {
|
||||
"label": "Lokale Änderungen speichern"
|
||||
},
|
||||
"sync-warning": "Um diesen synchronisierten Benutzer zu bearbeiten, aktivieren Sie die lokale Änderungsspeicherung. Andernfalls werden Ihre Änderungen bei der nächsten Synchronisierung überschrieben."
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Konfiguration für Schnittstelle:"
|
||||
@@ -473,9 +425,6 @@
|
||||
"disabled": {
|
||||
"label": "Schnittstelle deaktiviert"
|
||||
},
|
||||
"create-default-peer": {
|
||||
"label": "Peer für neue Benutzer automatisch erstellen"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "wg-quick Konfiguration automatisch speichern"
|
||||
},
|
||||
|
||||
@@ -42,8 +42,7 @@
|
||||
"audit": "Audit Log",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"keygen": "Key Generator",
|
||||
"calculator": "IP Calculator"
|
||||
"keygen": "Key Generator"
|
||||
},
|
||||
"home": {
|
||||
"headline": "WireGuard® VPN Portal",
|
||||
@@ -129,11 +128,6 @@
|
||||
"button-add-peers": "Add Multiple Peers",
|
||||
"button-show-peer": "Show Peer",
|
||||
"button-edit-peer": "Edit Peer",
|
||||
"button-bulk-delete": "Delete selected peers",
|
||||
"button-bulk-enable": "Enable selected peers",
|
||||
"button-bulk-disable": "Disable selected peers",
|
||||
"confirm-bulk-delete": "Are you sure you want to delete {count} peers?",
|
||||
"confirm-bulk-disable": "Are you sure you want to disable {count} peers?",
|
||||
"peer-disabled": "Peer is disabled, reason:",
|
||||
"peer-expiring": "Peer is expiring at",
|
||||
"peer-connected": "Connected",
|
||||
@@ -147,7 +141,7 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Firstname",
|
||||
"lastname": "Lastname",
|
||||
"sources": "Sources",
|
||||
"source": "Source",
|
||||
"peers": "Peers",
|
||||
"admin": "Admin"
|
||||
},
|
||||
@@ -158,14 +152,6 @@
|
||||
"button-add-user": "Add User",
|
||||
"button-show-user": "Show User",
|
||||
"button-edit-user": "Edit User",
|
||||
"button-bulk-delete": "Delete selected users",
|
||||
"button-bulk-enable": "Enable selected users",
|
||||
"button-bulk-disable": "Disable selected users",
|
||||
"button-bulk-lock": "Lock selected users",
|
||||
"button-bulk-unlock": "Unlock selected users",
|
||||
"confirm-bulk-delete": "Are you sure you want to delete {count} users?",
|
||||
"confirm-bulk-disable": "Are you sure you want to disable {count} users?",
|
||||
"confirm-bulk-lock": "Are you sure you want to lock {count} users?",
|
||||
"user-disabled": "User is disabled, reason:",
|
||||
"user-locked": "Account is locked, reason:",
|
||||
"admin": "User has administrator privileges",
|
||||
@@ -235,16 +221,6 @@
|
||||
"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."
|
||||
},
|
||||
"password": {
|
||||
"headline": "Password Settings",
|
||||
"abstract": "Here you can change your password.",
|
||||
"current-label": "Current Password",
|
||||
"new-label": "New Password",
|
||||
"new-confirm-label": "Confirm New Password",
|
||||
"change-button-text": "Change Password",
|
||||
"invalid-confirm-label": "Passwords do not match",
|
||||
"weak-label": "Password is too weak"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
@@ -283,26 +259,6 @@
|
||||
"placeholder": "The pre-shared key"
|
||||
}
|
||||
},
|
||||
"calculator": {
|
||||
"headline": "WireGuard IP Calculator",
|
||||
"abstract": "Generate a WireGuard Allowed IPs. The IP subnets are generated in your local browser and are never sent to the server.",
|
||||
"headline-allowed-ip": "New Allowed IPs",
|
||||
"button-exclude-private": "Exclude Private IP Ranges",
|
||||
"allowed-ip": {
|
||||
"label": "Allowed IPs",
|
||||
"placeholder": "0.0.0.0/0, ::/0",
|
||||
"empty": "Value cannot be empty"
|
||||
},
|
||||
"dissallowed-ip": {
|
||||
"label": "Disallowed IPs",
|
||||
"placeholder": "10.0.0.0/8, 192.168.0.0/16",
|
||||
"invalid": "Invalid address: {addr}"
|
||||
},
|
||||
"new-allowed-ip": {
|
||||
"label": "Allowed IPs",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "User Account:",
|
||||
@@ -378,11 +334,7 @@
|
||||
},
|
||||
"admin": {
|
||||
"label": "Is Admin"
|
||||
},
|
||||
"persist-local-changes": {
|
||||
"label": "Persist local changes"
|
||||
},
|
||||
"sync-warning": "To modify this synchronized user, enable local change persistence. Otherwise, your changes will be overwritten during the next synchronization."
|
||||
}
|
||||
},
|
||||
"interface-view": {
|
||||
"headline": "Config for Interface:"
|
||||
@@ -473,9 +425,6 @@
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
},
|
||||
"create-default-peer": {
|
||||
"label": "Create default peer for new users"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
},
|
||||
|
||||
@@ -2,26 +2,6 @@
|
||||
"languages": {
|
||||
"es": "Español"
|
||||
},
|
||||
"calculator": {
|
||||
"abstract": "Genera direcciones IP permitidas de WireGuard. Las subredes IP se generan en tu navegador local y nunca se envían al servidor.",
|
||||
"allowed-ip": {
|
||||
"empty": "El valor no puede estar vacío",
|
||||
"label": "IPs permitidas",
|
||||
"placeholder": "0.0.0.0/0, ::/0"
|
||||
},
|
||||
"button-exclude-private": "Excluir rangos de IP privadas",
|
||||
"dissallowed-ip": {
|
||||
"invalid": "Dirección inválida: {addr}",
|
||||
"label": "IPs no permitidas",
|
||||
"placeholder": "10.0.0.0/8, 192.168.0.0/16"
|
||||
},
|
||||
"headline": "Calculadora de IPs de WireGuard",
|
||||
"headline-allowed-ip": "Nuevas IPs permitidas",
|
||||
"new-allowed-ip": {
|
||||
"label": "IPs permitidas",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"pagination": {
|
||||
"size": "Numero de elementos",
|
||||
@@ -53,7 +33,6 @@
|
||||
"button-webauthn": "Usar clave de acceso"
|
||||
},
|
||||
"menu": {
|
||||
"calculator": "Calculadora IP",
|
||||
"home": "Inicio",
|
||||
"interfaces": "Interfaces",
|
||||
"users": "Usuarios",
|
||||
@@ -90,7 +69,7 @@
|
||||
"profiles": {
|
||||
"headline": "Perfiles VPN",
|
||||
"abstract": "Puedes acceder y descargar tus configuraciones personales de VPN desde tu perfil de usuario.",
|
||||
"content": "Para ver todos tus perfiles configurados, haz clic en el botón de abajo.",
|
||||
"content": "para ver todos tus perfiles configurados, haz clic en el botón de abajo.",
|
||||
"button": "Abrir mi perfil"
|
||||
},
|
||||
"admin": {
|
||||
@@ -117,7 +96,7 @@
|
||||
"table-heading": {
|
||||
"name": "Nombre",
|
||||
"user": "Usuario",
|
||||
"ip": "IPs",
|
||||
"ip": "IP's",
|
||||
"endpoint": "Endpoint",
|
||||
"status": "Estado"
|
||||
},
|
||||
@@ -135,7 +114,6 @@
|
||||
"total-endpoints": "Endpoints totales",
|
||||
"ip": "Dirección IP",
|
||||
"default-allowed-ip": "IPs permitidas por defecto",
|
||||
"default-dns": "Servidores DNS por defecto",
|
||||
"dns": "Servidores DNS",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Intervalo Keepalive por defecto",
|
||||
@@ -162,7 +140,7 @@
|
||||
"email": "Correo electrónico",
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"sources": "Origen",
|
||||
"source": "Origen",
|
||||
"peers": "Peers",
|
||||
"admin": "Administrador"
|
||||
},
|
||||
@@ -182,7 +160,7 @@
|
||||
"headline": "Mis peers VPN",
|
||||
"table-heading": {
|
||||
"name": "Nombre",
|
||||
"ip": "IPs",
|
||||
"ip": "IP's",
|
||||
"stats": "Estado",
|
||||
"interface": "Interfaz del servidor"
|
||||
},
|
||||
@@ -242,16 +220,6 @@
|
||||
"button-delete-text": "Eliminar la llave de acceso. Ya no podrás iniciar sesión con ella.",
|
||||
"button-register-title": "Registrar llave de acceso",
|
||||
"button-register-text": "Registrar una nueva llave de acceso para proteger tu cuenta."
|
||||
},
|
||||
"password": {
|
||||
"headline": "Configuración de contraseña",
|
||||
"abstract": "Aquí puedes cambiar tu contraseña.",
|
||||
"current-label": "Contraseña actual",
|
||||
"new-label": "Nueva contraseña",
|
||||
"new-confirm-label": "Confirmar nueva contraseña",
|
||||
"change-button-text": "Cambiar contraseña",
|
||||
"invalid-confirm-label": "Las contraseñas no coinciden",
|
||||
"weak-label": "La contraseña es demasiado débil"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
@@ -301,7 +269,7 @@
|
||||
"firstname": "Nombre",
|
||||
"lastname": "Apellido",
|
||||
"phone": "Número de Teléfono",
|
||||
"department": "Departamento",
|
||||
"depeertment": "Departamento",
|
||||
"api-enabled": "Acceso API",
|
||||
"disabled": "Cuenta Deshabilitada",
|
||||
"locked": "Cuenta Bloqueada",
|
||||
@@ -309,7 +277,7 @@
|
||||
"peers": {
|
||||
"name": "Nombre",
|
||||
"interface": "Interfaz",
|
||||
"ip": "IPs"
|
||||
"ip": "IP's"
|
||||
}
|
||||
},
|
||||
"user-edit": {
|
||||
@@ -341,7 +309,7 @@
|
||||
"label": "Teléfono",
|
||||
"placeholder": "El número de teléfono"
|
||||
},
|
||||
"department": {
|
||||
"depeertment": {
|
||||
"label": "Departamento",
|
||||
"placeholder": "El departamento"
|
||||
},
|
||||
@@ -370,16 +338,6 @@
|
||||
"interface-view": {
|
||||
"headline": "Configuración de la interfaz:"
|
||||
},
|
||||
"password": {
|
||||
"abstract": "Aquí puedes cambiar tu contraseña.",
|
||||
"change-button-text": "Cambiar contraseña",
|
||||
"current-label": "Contraseña actual",
|
||||
"headline": "Configuración de contraseña",
|
||||
"invalid-confirm-label": "Las contraseñas no coinciden",
|
||||
"new-confirm-label": "Confirmar nueva contraseña",
|
||||
"new-label": "Nueva contraseña",
|
||||
"weak-label": "La contraseña es demasiado débil"
|
||||
},
|
||||
"interface-edit": {
|
||||
"headline-edit": "Editar interfaz:",
|
||||
"headline-new": "Nueva interfaz",
|
||||
@@ -503,8 +461,6 @@
|
||||
"section-config": "Configuración",
|
||||
"identifier": "Identificador",
|
||||
"ip": "Direcciones IP",
|
||||
"allowed-ip": "Direcciones IP permitidas",
|
||||
"extra-allowed-ip": "Direcciones IP permitidas del lado del servidor",
|
||||
"user": "Usuario Asociado",
|
||||
"notes": "Notas",
|
||||
"expiry-status": "Expira en",
|
||||
@@ -513,10 +469,10 @@
|
||||
"connection-status": "Estadísticas de Conexión",
|
||||
"upload": "Bytes Subidos (del Servidor al peer)",
|
||||
"download": "Bytes Descargados (del peer al Servidor)",
|
||||
"pingable": "Alcanzable (ping)",
|
||||
"handshake": "Último handshake",
|
||||
"pingable": "Es Alcanzable (Ping)",
|
||||
"handshake": "Último Handshake",
|
||||
"connected-since": "Conectado desde",
|
||||
"endpoint": "Dirección del host remoto",
|
||||
"endpoint": "Endpoint",
|
||||
"button-download": "Descargar configuración",
|
||||
"button-email": "Enviar configuración por Correo Electrónico",
|
||||
"style-label": "Estilo de Configuración"
|
||||
@@ -532,7 +488,7 @@
|
||||
"header-hooks": "Hooks (Ejecutados en el peer)",
|
||||
"header-state": "Estado",
|
||||
"display-name": {
|
||||
"label": "Nombre para mostrar",
|
||||
"label": "Nombre para Mostrar",
|
||||
"placeholder": "El nombre descriptivo para el peer"
|
||||
},
|
||||
"linked-user": {
|
||||
@@ -545,7 +501,7 @@
|
||||
"help": "La clave privada se almacena de forma segura en el servidor. Si el usuario ya posee una copia, puedes omitir este campo. El servidor sigue funcionando exclusivamente con la clave pública del peer."
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Clave Pública",
|
||||
"label": "Cave Pública",
|
||||
"placeholder": "La Clave pública"
|
||||
},
|
||||
"preshared-key": {
|
||||
@@ -556,10 +512,6 @@
|
||||
"label": "Dirección del endpoint",
|
||||
"placeholder": "La dirección del endpoint remoto"
|
||||
},
|
||||
"endpoint-public-key": {
|
||||
"label": "Clave pública del punto del endpoint",
|
||||
"placeholder": "La clave pública del endpoint remoto"
|
||||
},
|
||||
"ip": {
|
||||
"label": "Direcciones IP",
|
||||
"placeholder": "Direcciones IP (formato CIDR)"
|
||||
@@ -624,11 +576,11 @@
|
||||
"description": "Un identificador de usuario (el nombre de usuario) para el cual debe crearse un peer."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Peer:",
|
||||
"headline-peer": "peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Prefijo del nombre del peer a mostrar",
|
||||
"placeholder": "Prefijo",
|
||||
"description": "Un prefijo que se agregará al nombre visible de los peers."
|
||||
"label": "Prefijo del Nombre peera Mostrar",
|
||||
"placeholder": "El prefijo",
|
||||
"description": "Un prefijo que se agregará al nombre mostrado de los peers."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
"email": "E-mail",
|
||||
"firstname": "Prénom",
|
||||
"lastname": "Nom",
|
||||
"sources": "Sources",
|
||||
"source": "Source",
|
||||
"peers": "Pairs",
|
||||
"admin": "Admin"
|
||||
},
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
"email": "이메일",
|
||||
"firstname": "이름",
|
||||
"lastname": "성",
|
||||
"sources": "소스",
|
||||
"source": "소스",
|
||||
"peers": "피어",
|
||||
"admin": "관리자"
|
||||
},
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Primeiro Nome",
|
||||
"lastname": "Último Nome",
|
||||
"sources": "Fonte",
|
||||
"source": "Fonte",
|
||||
"peers": "Peers",
|
||||
"admin": "Administrador"
|
||||
},
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
"label": "Пароль",
|
||||
"placeholder": "Пожалуйста, введите ваш пароль"
|
||||
},
|
||||
"button": "Войти",
|
||||
"button-webauthn": "Использовать Passkey"
|
||||
"button": "Войти"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Главная",
|
||||
@@ -38,12 +37,8 @@
|
||||
"users": "Пользователи",
|
||||
"lang": "Сменить язык",
|
||||
"profile": "Мой профиль",
|
||||
"settings": "Настройки",
|
||||
"audit": "Журнал аудита",
|
||||
"login": "Вход",
|
||||
"logout": "Выход",
|
||||
"keygen": "Генератор ключей",
|
||||
"calculator": "Калькулятор IP-адресов"
|
||||
"logout": "Выход"
|
||||
},
|
||||
"home": {
|
||||
"headline": "Портал VPN WireGuard®",
|
||||
@@ -105,8 +100,6 @@
|
||||
"interface": {
|
||||
"headline": "Статус интерфейса для",
|
||||
"backend": "бэкэнд",
|
||||
"unknown-backend": "Неизвестно",
|
||||
"wrong-backend": "Неверный бэкэнд, вместо него используется локальный сервер WireGuard!",
|
||||
"key": "Публичный ключ",
|
||||
"endpoint": "Публичная конечная точка",
|
||||
"port": "Порт прослушивания",
|
||||
@@ -119,7 +112,6 @@
|
||||
"dns": "DNS-серверы",
|
||||
"mtu": "MTU",
|
||||
"default-keep-alive": "Интервал поддержания активности по умолчанию",
|
||||
"default-dns": "DNS-сервера по-умолчанию",
|
||||
"button-show-config": "Показать конфигурацию",
|
||||
"button-download-config": "Скачать конфигурацию",
|
||||
"button-store-config": "Сохранить конфигурацию для wg-quick",
|
||||
@@ -143,7 +135,7 @@
|
||||
"email": "Электронная почта",
|
||||
"firstname": "Имя",
|
||||
"lastname": "Фамилия",
|
||||
"sources": "Источник",
|
||||
"source": "Источник",
|
||||
"peers": "Пиры",
|
||||
"admin": "Админ"
|
||||
},
|
||||
@@ -176,121 +168,6 @@
|
||||
"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"
|
||||
},
|
||||
"webauthn": {
|
||||
"headline": "Настройки Passkey",
|
||||
"abstract": "Passkey - это современный способ аутентификации пользователей без использования паролей. Он надежно хранятся в вашем браузере и могут быть использованы для входа в WireGuard Portal.",
|
||||
"active-description": "В данный момент для вашей учетной записи пользователя активен по крайней мере один Passkey.",
|
||||
"inactive-description": "В настоящее время для вашей учетной записи пользователя не зарегистрировано ни одного Passkey. Нажмите кнопку ниже, чтобы зарегистрировать новый Passkey.",
|
||||
"table": {
|
||||
"name": "Название",
|
||||
"created": "Создано",
|
||||
"actions": ""
|
||||
},
|
||||
"credentials-list": "Зарегистрированные Passkeys",
|
||||
"modal-delete": {
|
||||
"headline": "Удалить Passkey",
|
||||
"abstract": "Вы уверены, что хотите удалить этот Passkey? Вы больше не сможете войти в систему с помощью этого Passkey.",
|
||||
"created": "Создано:",
|
||||
"button-delete": "Удалить",
|
||||
"button-cancel": "Отмена"
|
||||
},
|
||||
"button-rename-title": "Переименновать",
|
||||
"button-rename-text": "Переименновать Passkey.",
|
||||
"button-save-title": "Сохранить",
|
||||
"button-save-text": "Сохранить новое название Passkey.",
|
||||
"button-cancel-title": "Отмена",
|
||||
"button-cancel-text": "Отмена переименования Passkey.",
|
||||
"button-delete-title": "Удалить",
|
||||
"button-delete-text": "Удалить Passkey. Вы больше не сможете войти в систему с помощью этого Passkey.",
|
||||
"button-register-title": "Зарегистрировать Passkey",
|
||||
"button-register-text": "Зарегистрировать Passkey, чтобы защитить свою учетную запись."
|
||||
},
|
||||
"password": {
|
||||
"headline": "Настройки пароля",
|
||||
"abstract": "Здесь можете изменить свой пароль.",
|
||||
"current-label": "Текущий пароль",
|
||||
"new-label": "Новый пароль",
|
||||
"new-confirm-label": "Повторно новый пароль",
|
||||
"change-button-text": "Изменить пароль",
|
||||
"invalid-confirm-label": "Пароли не совпадают",
|
||||
"weak-label": "Пароль слишком простой"
|
||||
}
|
||||
},
|
||||
"audit": {
|
||||
"headline": "Журнал аудита",
|
||||
"abstract": "Здесь вы можете ознакомиться с журналом аудита всех действий, выполненных на WireGuard Portal.",
|
||||
"no-entries": {
|
||||
"headline": "Нет доступных записей в журнале",
|
||||
"abstract": "В данный момент, журнал аудита пуст."
|
||||
},
|
||||
"entries-headline": "Записи журнала",
|
||||
"table-heading": {
|
||||
"id": "#",
|
||||
"time": "Время",
|
||||
"user": "Пользователь",
|
||||
"severity": "Серьезность",
|
||||
"origin": "Источник",
|
||||
"message": "Сообщение"
|
||||
}
|
||||
},
|
||||
"keygen": {
|
||||
"headline": "Генератор WireGuard-ключей",
|
||||
"abstract": "Генерация WireGuard-ключей. Ключи генерируются в вашем локальном браузере и никогда не отправляются на сервер.",
|
||||
"headline-keypair": "Новая пара ключей",
|
||||
"headline-preshared-key": "Новый общий ключ",
|
||||
"button-generate": "Генерировать",
|
||||
"private-key": {
|
||||
"label": "Приватный ключ",
|
||||
"placeholder": "Приватный ключ"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Публичный ключ",
|
||||
"placeholder": "Публичный ключ"
|
||||
},
|
||||
"preshared-key": {
|
||||
"label": "Общий ключ",
|
||||
"placeholder": "Общий ключ"
|
||||
}
|
||||
},
|
||||
"calculator": {
|
||||
"headline": "Калькулятор IP-адресов",
|
||||
"abstract": "Генерация разрешенных IP-адресов. IP-подсети генерируются в вашем локальном браузере и никогда не отправляются на сервер.",
|
||||
"headline-allowed-ip": "Новые разрешенные IP-адреса",
|
||||
"button-exclude-private": "Исключить частные диапазоны IP-адресов",
|
||||
"allowed-ip": {
|
||||
"label": "Разрешенные IP-адреса",
|
||||
"placeholder": "0.0.0.0/0, ::/0",
|
||||
"empty": "Поле ввода не должно быть пустым"
|
||||
},
|
||||
"dissallowed-ip": {
|
||||
"label": "Запрещенные IP-адреса",
|
||||
"placeholder": "10.0.0.0/8, 192.168.0.0/16",
|
||||
"invalid": "Некорректный адрес: {addr}"
|
||||
},
|
||||
"new-allowed-ip": {
|
||||
"label": "Разрешенные IP-адреса",
|
||||
"placeholder": ""
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"user-view": {
|
||||
"headline": "Учетная запись пользователя:",
|
||||
@@ -303,7 +180,6 @@
|
||||
"lastname": "Фамилия",
|
||||
"phone": "Номер телефона",
|
||||
"department": "Отдел",
|
||||
"api-enabled": "API",
|
||||
"disabled": "Учетная запись отключена",
|
||||
"locked": "Учетная запись заблокирована",
|
||||
"no-peers": "У пользователя нет связанных пиров.",
|
||||
@@ -331,8 +207,7 @@
|
||||
"password": {
|
||||
"label": "Пароль",
|
||||
"placeholder": "Надежный пароль",
|
||||
"description": "Оставьте это поле пустым, чтобы сохранить текущий пароль.",
|
||||
"too-weak": "Пароль слишком простой. Используйте более сложный пароль."
|
||||
"description": "Оставьте это поле пустым, чтобы сохранить текущий пароль."
|
||||
},
|
||||
"email": {
|
||||
"label": "Электронная почта",
|
||||
@@ -392,11 +267,6 @@
|
||||
"client": "Режим клиента",
|
||||
"any": "Неизвестный режим"
|
||||
},
|
||||
"backend": {
|
||||
"label": "Бэкэнд интерфейса",
|
||||
"invalid-label": "Оригинальный бэкэнд больше недоступн, вместо нее используется локальная WireGuard-бэкэнд!",
|
||||
"local": "Локальный WireGuard-бэкэнд"
|
||||
},
|
||||
"display-name": {
|
||||
"label": "Отображаемое имя",
|
||||
"placeholder": "Описательное имя для интерфейса"
|
||||
@@ -494,8 +364,6 @@
|
||||
"section-config": "Конфигурация",
|
||||
"identifier": "Идентификатор",
|
||||
"ip": "IP-адреса",
|
||||
"allowed-ip": "Разрешённые IP-адреса",
|
||||
"extra-allowed-ip": "Разрешённые IP-адреса на стороне сервера",
|
||||
"user": "Связанный пользователь",
|
||||
"notes": "Заметки",
|
||||
"expiry-status": "Истекает в",
|
||||
@@ -508,10 +376,8 @@
|
||||
"handshake": "Последнее рукопожатие",
|
||||
"connected-since": "Подключен с",
|
||||
"endpoint": "Конечная точка",
|
||||
"endpoint-key": "Публичный ключ конечной точки",
|
||||
"button-download": "Скачать конфигурацию",
|
||||
"button-email": "Отправить конфигурацию по электронной почте",
|
||||
"style-label": "Вид конфигурации"
|
||||
"button-email": "Отправить конфигурацию по электронной почте"
|
||||
},
|
||||
"peer-edit": {
|
||||
"headline-edit-peer": "Редактировать пира:",
|
||||
@@ -533,8 +399,7 @@
|
||||
},
|
||||
"private-key": {
|
||||
"label": "Приватный ключ",
|
||||
"placeholder": "Приватный ключ",
|
||||
"help": "Закрытый ключ надежно хранится на сервере. Если у пользователя уже есть копия, вы можете не указывать это поле. Сервер работает исключительно с открытым ключом клиента."
|
||||
"placeholder": "Приватный ключ"
|
||||
},
|
||||
"public-key": {
|
||||
"label": "Публичный ключ",
|
||||
@@ -566,61 +431,61 @@
|
||||
"description": "Эти IP-адреса будут добавлены в удаленный интерфейс WireGuard как разрешенные IP-адреса."
|
||||
},
|
||||
"dns": {
|
||||
"label": "DNS-сервер",
|
||||
"placeholder": "Используемые DNS-серверы"
|
||||
"label": "DNS Server",
|
||||
"placeholder": "The DNS servers that should be used"
|
||||
},
|
||||
"dns-search": {
|
||||
"label": "Поисковые домены DNS",
|
||||
"placeholder": "Префиксы поиска DNS"
|
||||
"label": "DNS Search Domains",
|
||||
"placeholder": "DNS search prefixes"
|
||||
},
|
||||
"keep-alive": {
|
||||
"label": "Интервал поддержания активности",
|
||||
"placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)"
|
||||
"label": "Keep Alive Interval",
|
||||
"placeholder": "Persistent Keepalive (0 = default)"
|
||||
},
|
||||
"mtu": {
|
||||
"label": "MTU",
|
||||
"placeholder": "MTU клиента (0 = использовать значение по умолчанию)"
|
||||
"placeholder": "The client MTU (0 = keep default)"
|
||||
},
|
||||
"pre-up": {
|
||||
"label": "Pre-Up",
|
||||
"placeholder": "Одна или несколько команд bash, разделенных ;"
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-up": {
|
||||
"label": "Post-Up",
|
||||
"placeholder": "Одна или несколько команд bash, разделенных ;"
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"pre-down": {
|
||||
"label": "Pre-Down",
|
||||
"placeholder": "Одна или несколько команд bash, разделенных ;"
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"post-down": {
|
||||
"label": "Post-Down",
|
||||
"placeholder": "Одна или несколько команд bash, разделенных ;"
|
||||
"placeholder": "One or multiple bash commands separated by ;"
|
||||
},
|
||||
"disabled": {
|
||||
"label": "Узел отключен"
|
||||
"label": "Peer Disabled"
|
||||
},
|
||||
"ignore-global": {
|
||||
"label": "Игнорировать глобальные настройки"
|
||||
"label": "Ignore global settings"
|
||||
},
|
||||
"expires-at": {
|
||||
"label": "Дата истечения срока действия"
|
||||
"label": "Expiry date"
|
||||
}
|
||||
},
|
||||
"peer-multi-create": {
|
||||
"headline-peer": "Создать несколько узлов",
|
||||
"headline-endpoint": "Создать несколько конечных точек",
|
||||
"headline-peer": "Create multiple peers",
|
||||
"headline-endpoint": "Create multiple endpoints",
|
||||
"identifiers": {
|
||||
"label": "Идентификаторы пользователей",
|
||||
"placeholder": "Идентификаторы пользователей",
|
||||
"description": "Идентификатор пользователя (имя пользователя), для которого узел будет создан."
|
||||
"label": "User Identifiers",
|
||||
"placeholder": "User Identifiers",
|
||||
"description": "A user identifier (the username) for which a peer should be created."
|
||||
},
|
||||
"prefix": {
|
||||
"headline-peer": "Узел:",
|
||||
"headline-endpoint": "Конечная точка:",
|
||||
"label": "Префикс отображаемого имени",
|
||||
"placeholder": "Префикс",
|
||||
"description": "Префикс будет добавлен к отображаемому имени узла."
|
||||
"headline-peer": "Peer:",
|
||||
"headline-endpoint": "Endpoint:",
|
||||
"label": "Display Name Prefix",
|
||||
"placeholder": "The prefix",
|
||||
"description": "A prefix that is added to the peers display name."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Ім'я",
|
||||
"lastname": "Прізвище",
|
||||
"sources": "Джерело",
|
||||
"source": "Джерело",
|
||||
"peers": "Піри",
|
||||
"admin": "Адміністратор"
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"email": "E-Mail",
|
||||
"firstname": "Tên",
|
||||
"lastname": "Họ",
|
||||
"sources": "Nguồn",
|
||||
"source": "Nguồn",
|
||||
"peers": "Peers",
|
||||
"admin": "Quản trị viên"
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"email": "电子邮件",
|
||||
"firstname": "名",
|
||||
"lastname": "姓",
|
||||
"sources": "来源",
|
||||
"source": "来源",
|
||||
"peers": "节点",
|
||||
"admin": "管理员"
|
||||
},
|
||||
|
||||
@@ -72,14 +72,6 @@ const router = createRouter({
|
||||
// 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')
|
||||
},
|
||||
{
|
||||
path: '/ip-calculator',
|
||||
name: 'ip-calculator',
|
||||
// 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/IPCalculatorView.vue')
|
||||
}
|
||||
],
|
||||
linkActiveClass: "active",
|
||||
@@ -130,7 +122,7 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
|
||||
// redirect to login page if not logged in and trying to access a restricted page
|
||||
const publicPages = ['/', '/login', '/key-generator', '/ip-calculator']
|
||||
const publicPages = ['/', '/login', '/key-generator']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
|
||||
if (authRequired && !auth.IsAuthenticated) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
|
||||
|
||||
import { notify } from "@kyvg/vue3-notification";
|
||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||
import { websocketWrapper } from '@/helpers/websocket-wrapper'
|
||||
import router from '../router'
|
||||
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||
import {base64_url_encode} from "@/helpers/encoding";
|
||||
@@ -296,11 +295,9 @@ export const authStore = defineStore('auth',{
|
||||
}
|
||||
}
|
||||
localStorage.setItem('user', JSON.stringify(this.user))
|
||||
websocketWrapper.connect()
|
||||
} else {
|
||||
this.user = null
|
||||
localStorage.removeItem('user')
|
||||
websocketWrapper.disconnect()
|
||||
}
|
||||
},
|
||||
setWebAuthnCredentials(credentials) {
|
||||
|
||||
@@ -14,7 +14,6 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
configuration: "",
|
||||
selected: "",
|
||||
fetching: false,
|
||||
trafficStats: {},
|
||||
}),
|
||||
getters: {
|
||||
Count: (state) => state.interfaces.length,
|
||||
@@ -25,9 +24,6 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
},
|
||||
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
|
||||
isFetching: (state) => state.fetching,
|
||||
TrafficStats: (state) => {
|
||||
return (state.selected in state.trafficStats) ? state.trafficStats[state.selected] : { Received: 0, Transmitted: 0 }
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setInterfaces(interfaces) {
|
||||
@@ -38,14 +34,6 @@ export const interfaceStore = defineStore('interfaces', {
|
||||
this.selected = ""
|
||||
}
|
||||
this.fetching = false
|
||||
this.trafficStats = {}
|
||||
},
|
||||
updateInterfaceTrafficStats(interfaceStats) {
|
||||
const id = interfaceStats.EntityId;
|
||||
this.trafficStats[id] = {
|
||||
Received: interfaceStats.BytesReceived,
|
||||
Transmitted: interfaceStats.BytesTransmitted,
|
||||
};
|
||||
},
|
||||
async LoadInterfaces() {
|
||||
this.fetching = true
|
||||
|
||||
@@ -23,7 +23,6 @@ export const peerStore = defineStore('peers', {
|
||||
fetching: false,
|
||||
sortKey: 'IsConnected', // Default sort key
|
||||
sortOrder: -1, // 1 for ascending, -1 for descending
|
||||
trafficStats: {},
|
||||
}),
|
||||
getters: {
|
||||
Find: (state) => {
|
||||
@@ -77,9 +76,6 @@ export const peerStore = defineStore('peers', {
|
||||
Statistics: (state) => {
|
||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||
},
|
||||
TrafficStats: (state) => {
|
||||
return (id) => (id in state.trafficStats) ? state.trafficStats[id] : { Received: 0, Transmitted: 0 }
|
||||
},
|
||||
hasStatistics: (state) => state.statsEnabled,
|
||||
|
||||
},
|
||||
@@ -115,7 +111,6 @@ export const peerStore = defineStore('peers', {
|
||||
this.peers = peers
|
||||
this.calculatePages()
|
||||
this.fetching = false
|
||||
this.trafficStats = {}
|
||||
},
|
||||
setPeer(peer) {
|
||||
this.peer = peer
|
||||
@@ -131,19 +126,11 @@ export const peerStore = defineStore('peers', {
|
||||
if (!statsResponse) {
|
||||
this.stats = {}
|
||||
this.statsEnabled = false
|
||||
this.trafficStats = {}
|
||||
} else {
|
||||
this.stats = statsResponse.Stats
|
||||
this.statsEnabled = statsResponse.Enabled
|
||||
}
|
||||
},
|
||||
updatePeerTrafficStats(peerStats) {
|
||||
const id = peerStats.EntityId;
|
||||
this.trafficStats[id] = {
|
||||
Received: peerStats.BytesReceived,
|
||||
Transmitted: peerStats.BytesTransmitted,
|
||||
};
|
||||
},
|
||||
async Reset() {
|
||||
this.setPeers([])
|
||||
this.setStats(undefined)
|
||||
@@ -235,73 +222,6 @@ export const peerStore = defineStore('peers', {
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkDelete(ids) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-delete`, { Identifiers: ids })
|
||||
.then(() => {
|
||||
this.peers = this.peers.filter(p => !ids.includes(p.Identifier))
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Peers deleted",
|
||||
text: "Selected peers have been deleted!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to delete peers: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to delete selected peers!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkEnable(ids) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-enable`, { Identifiers: ids })
|
||||
.then(async () => {
|
||||
await this.LoadPeers()
|
||||
notify({
|
||||
title: "Peers enabled",
|
||||
text: "Selected peers have been enabled!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to enable peers: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to enable selected peers!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkDisable(ids, reason) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-disable`, { Identifiers: ids, Reason: reason })
|
||||
.then(async () => {
|
||||
await this.LoadPeers()
|
||||
notify({
|
||||
title: "Peers disabled",
|
||||
text: "Selected peers have been disabled!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to disable peers: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to disable selected peers!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async UpdatePeer(id, formData) {
|
||||
this.fetching = true
|
||||
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
||||
|
||||
@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
|
||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {authStore} from "@/stores/auth";
|
||||
import {peerStore} from "@/stores/peers";
|
||||
import { base64_url_encode } from '@/helpers/encoding';
|
||||
import {freshStats} from "@/helpers/models";
|
||||
import { ipToBigInt } from '@/helpers/utils';
|
||||
@@ -152,17 +151,6 @@ export const profileStore = defineStore('profile', {
|
||||
})
|
||||
})
|
||||
},
|
||||
async changePassword(formData) {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/change-password`, formData)
|
||||
.then(this.fetching = false)
|
||||
.catch(error => {
|
||||
this.fetching = false;
|
||||
console.log("Failed to change password for ", currentUser, ": ", error);
|
||||
throw new Error(error);
|
||||
});
|
||||
},
|
||||
async LoadPeers() {
|
||||
this.fetching = true
|
||||
let currentUser = authStore().user.Identifier
|
||||
@@ -219,18 +207,5 @@ export const profileStore = defineStore('profile', {
|
||||
})
|
||||
})
|
||||
},
|
||||
async BulkDelete(ids) {
|
||||
this.fetching = true
|
||||
const peers = peerStore()
|
||||
return peers.BulkDelete(ids)
|
||||
.then(() => {
|
||||
this.peers = this.peers.filter(p => !ids.includes(p.Identifier))
|
||||
this.fetching = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -142,140 +142,5 @@ export const userStore = defineStore('users', {
|
||||
})
|
||||
})
|
||||
},
|
||||
async BulkDelete(ids) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-delete`, { Identifiers: ids })
|
||||
.then(() => {
|
||||
this.users = this.users.filter(u => !ids.includes(u.Identifier))
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Users deleted",
|
||||
text: "Selected users have been deleted!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to delete users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to delete selected users!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkEnable(ids) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-enable`, { Identifiers: ids })
|
||||
.then(() => {
|
||||
this.users.forEach(u => {
|
||||
if (ids.includes(u.Identifier)) {
|
||||
u.Disabled = false
|
||||
u.DisabledReason = ""
|
||||
}
|
||||
})
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Users enabled",
|
||||
text: "Selected users have been enabled!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to enable users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to enable selected users!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkDisable(ids, reason) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-disable`, { Identifiers: ids, Reason: reason })
|
||||
.then(() => {
|
||||
this.users.forEach(u => {
|
||||
if (ids.includes(u.Identifier)) {
|
||||
u.Disabled = true
|
||||
u.DisabledReason = reason
|
||||
}
|
||||
})
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Users disabled",
|
||||
text: "Selected users have been disabled!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to disable users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to disable selected users!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkLock(ids, reason) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-lock`, { Identifiers: ids, Reason: reason })
|
||||
.then(() => {
|
||||
this.users.forEach(u => {
|
||||
if (ids.includes(u.Identifier)) {
|
||||
u.Locked = true
|
||||
u.LockedReason = reason
|
||||
}
|
||||
})
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Users locked",
|
||||
text: "Selected users have been locked!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to lock users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to lock selected users!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
async BulkUnlock(ids) {
|
||||
this.fetching = true
|
||||
return apiWrapper.post(`${baseUrl}/bulk-unlock`, { Identifiers: ids })
|
||||
.then(() => {
|
||||
this.users.forEach(u => {
|
||||
if (ids.includes(u.Identifier)) {
|
||||
u.Locked = false
|
||||
u.LockedReason = ""
|
||||
}
|
||||
})
|
||||
this.fetching = false
|
||||
notify({
|
||||
title: "Users unlocked",
|
||||
text: "Selected users have been unlocked!",
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
this.fetching = false
|
||||
console.log("Failed to unlock users: ", error)
|
||||
notify({
|
||||
title: "Backend Connection Failure",
|
||||
text: "Failed to unlock selected users!",
|
||||
type: 'error',
|
||||
})
|
||||
throw new Error(error)
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
import {ref, watch, computed} from "vue";
|
||||
import isCidr from "is-cidr";
|
||||
import {isIP} from "is-ip";
|
||||
import {excludeCidr} from "cidr-tools";
|
||||
import {useI18n} from 'vue-i18n';
|
||||
|
||||
const allowedIp = ref("")
|
||||
const dissallowedIp = ref("")
|
||||
const privateIP = ref("10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16")
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
const errorAllowed = ref("")
|
||||
const errorDissallowed = ref("")
|
||||
|
||||
/**
|
||||
* Validate a comma-separated list of IP and/or CIDR addresses.
|
||||
* @function validateIpAndCidrList
|
||||
* @param {string} value - Comma-separated string (e.g. "10.0.0.0/8, 192.168.0.1")
|
||||
* @returns {true|string} Returns true if all values are valid, otherwise an error message.
|
||||
*/
|
||||
function validateIpAndCidrList(value) {
|
||||
const list = value.split(",").map(v => v.trim()).filter(Boolean);
|
||||
if (list.length === 0) {
|
||||
return t('calculator.allowed-ip.empty');
|
||||
}
|
||||
|
||||
for (const addr of list) {
|
||||
if (!isIP(addr) && !isCidr(addr)) {
|
||||
return t('calculator.dissallowed-ip.invalid', {addr});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watcher that validates allowed IPs input in real-time.
|
||||
* Updates `errorAllowed` whenever `allowedIp` changes.
|
||||
*/
|
||||
watch(allowedIp, (newValue) => {
|
||||
const result = validateIpAndCidrList(newValue);
|
||||
errorAllowed.value = result === true ? "" : result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Watcher that validates disallowed IPs input in real-time.
|
||||
* Updates `errorDissallowed` whenever `dissallowedIp` changes.
|
||||
*/
|
||||
watch(dissallowedIp, (newValue) => {
|
||||
if (!allowedIp.value || allowedIp.value.trim() === "") {
|
||||
allowedIp.value = "0.0.0.0/0";
|
||||
}
|
||||
const result = validateIpAndCidrList(newValue);
|
||||
errorDissallowed.value = result === true ? "" : result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Dynamically computes the resulting "Allowed IPs" list
|
||||
* by excluding the disallowed ranges from the allowed ranges.
|
||||
* @constant
|
||||
* @type {ComputedRef<string>}
|
||||
* @returns {string} A comma-separated string of resulting CIDR blocks.
|
||||
*/
|
||||
const newAllowedIp = computed(() => {
|
||||
if (errorAllowed.value || errorDissallowed.value) return "";
|
||||
|
||||
try {
|
||||
const allowedList = allowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
|
||||
const disallowedList = dissallowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
|
||||
|
||||
const result = excludeCidr(allowedList, disallowedList);
|
||||
|
||||
return result.join(", ");
|
||||
} catch (e) {
|
||||
console.error("Allowed IPs calculation error:", e);
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Append private IP ranges to disallowed IPs.
|
||||
* If any already exist, they are preserved and new ones are appended only if not present.
|
||||
* @function addPrivateIPs
|
||||
*/
|
||||
function addPrivateIPs() {
|
||||
const privateList = privateIP.value.split(",").map(v => v.trim());
|
||||
const currentList = dissallowedIp.value.split(",").map(v => v.trim()).filter(Boolean);
|
||||
|
||||
const combined = Array.from(new Set([...currentList, ...privateList]));
|
||||
dissallowedIp.value = combined.join(", ");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-header">
|
||||
<h1>{{ $t('calculator.headline') }}</h1>
|
||||
</div>
|
||||
|
||||
<p class="lead">{{ $t('calculator.abstract') }}</p>
|
||||
|
||||
<div class="mt-4 row">
|
||||
<div class="col-12 col-lg-5">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('calculator.allowed-ip.label') }}</label>
|
||||
<input class="form-control" v-model="allowedIp" :placeholder="$t('calculator.allowed-ip.placeholder')" :class="{ 'is-invalid': errorAllowed }">
|
||||
<div v-if="errorAllowed" class="text-danger mt-1">{{ errorAllowed }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('calculator.dissallowed-ip.label') }}</label>
|
||||
<input class="form-control" v-model="dissallowedIp" :placeholder="$t('calculator.dissallowed-ip.placeholder')" :class="{ 'is-invalid': errorDissallowed }">
|
||||
<div v-if="errorDissallowed" class="text-danger mt-1">{{ errorDissallowed }}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<hr class="mt-4">
|
||||
<button class="btn btn-primary mb-4" type="button" @click="addPrivateIPs">{{ $t('calculator.button-exclude-private') }}</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="col-12 col-lg-2 mt-sm-4">
|
||||
</div>
|
||||
<div class="col-12 col-lg-5">
|
||||
<h1>{{ $t('calculator.headline-allowed-ip') }}</h1>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" :value="newAllowedIp" rows="6" :placeholder="$t('calculator.new-allowed-ip.placeholder')" readonly></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -29,10 +29,6 @@ const sortKey = ref("")
|
||||
const sortOrder = ref(1)
|
||||
const selectAll = ref(false)
|
||||
|
||||
const selectedPeers = computed(() => {
|
||||
return peers.All.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
|
||||
})
|
||||
|
||||
function sortBy(key) {
|
||||
if (sortKey.value === key) {
|
||||
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
||||
@@ -115,39 +111,6 @@ async function saveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
if (confirm(t('interfaces.confirm-bulk-delete', {count: selectedPeers.value.length}))) {
|
||||
try {
|
||||
await peers.BulkDelete(selectedPeers.value)
|
||||
selectAll.value = false // reset selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkEnable() {
|
||||
try {
|
||||
await peers.BulkEnable(selectedPeers.value)
|
||||
selectAll.value = false
|
||||
peers.All.forEach(p => p.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDisable() {
|
||||
if (confirm(t('interfaces.confirm-bulk-disable', {count: selectedPeers.value.length}))) {
|
||||
try {
|
||||
await peers.BulkDisable(selectedPeers.value)
|
||||
selectAll.value = false
|
||||
peers.All.forEach(p => p.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
peers.FilteredAndPaged.forEach(peer => {
|
||||
peer.IsSelected = selectAll.value;
|
||||
@@ -210,12 +173,6 @@ onMounted(async () => {
|
||||
<div class="col-12 col-lg-8">
|
||||
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }}<span v-if="!isBackendValid" :title="t('interfaces.interface.wrong-backend')" class="ms-1 me-1"><i class="fa-solid fa-triangle-exclamation"></i></span>)
|
||||
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||
<div v-if="interfaces.GetSelected && (interfaces.TrafficStats.Received > 0 || interfaces.TrafficStats.Transmitted > 0)" class="mt-2">
|
||||
<small class="text-muted">
|
||||
Traffic: <i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Received) }}/s
|
||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Transmitted) }}/s
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4 text-lg-end">
|
||||
<a class="btn-link" href="#" :title="$t('interfaces.interface.button-show-config')" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
||||
@@ -396,13 +353,6 @@ onMounted(async () => {
|
||||
<a class="btn btn-primary ms-2" href="#" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="selectedPeers.length > 0">
|
||||
<div class="col-12 text-lg-end">
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-enable')" @click.prevent="bulkEnable"><i class="fa-regular fa-circle-check"></i></a>
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-disable')" @click.prevent="bulkDisable"><i class="fa fa-ban"></i></a>
|
||||
<a class="btn btn-outline-danger btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-delete')" @click.prevent="bulkDelete"><i class="fa fa-trash-can"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="interfaces.Count!==0" class="mt-2 table-responsive">
|
||||
<div v-if="peers.Count===0">
|
||||
<h4>{{ $t('interfaces.no-peer.headline') }}</h4>
|
||||
@@ -457,19 +407,14 @@ onMounted(async () => {
|
||||
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
||||
<td v-if="peers.hasStatistics">
|
||||
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <small class="text-muted" :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake"><i class="fa-solid fa-circle-info"></i></small>
|
||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <span :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake">{{ $t('interfaces.peer-connected') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td v-if="peers.hasStatistics" >
|
||||
<div class="d-flex flex-column">
|
||||
<span :title="humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) + ' / ' + humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted)">
|
||||
<i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Received) }}/s
|
||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Transmitted) }}/s
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-center" >{{ humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted) }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
<script setup>
|
||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import { peerStore } from "@/stores/peers";
|
||||
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { humanFileSize } from "@/helpers/utils";
|
||||
|
||||
const settings = settingsStore()
|
||||
const profile = profileStore()
|
||||
const peers = peerStore()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const viewedPeerId = ref("")
|
||||
const editPeerId = ref("")
|
||||
@@ -22,10 +17,6 @@ const sortKey = ref("")
|
||||
const sortOrder = ref(1)
|
||||
const selectAll = ref(false)
|
||||
|
||||
const selectedPeers = computed(() => {
|
||||
return profile.Peers.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
|
||||
})
|
||||
|
||||
function sortBy(key) {
|
||||
if (sortKey.value === key) {
|
||||
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
||||
@@ -44,17 +35,6 @@ function friendlyInterfaceName(id, name) {
|
||||
return id
|
||||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
if (confirm(t('interfaces.confirm-bulk-delete', {count: selectedPeers.value.length}))) {
|
||||
try {
|
||||
await profile.BulkDelete(selectedPeers.value)
|
||||
selectAll.value = false // reset selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
profile.FilteredAndPagedPeers.forEach(peer => {
|
||||
peer.IsSelected = selectAll.value;
|
||||
@@ -104,13 +84,6 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="selectedPeers.length > 0">
|
||||
<div class="col-12 text-lg-end">
|
||||
<button class="btn btn-outline-danger btn-sm" :title="$t('interfaces.button-bulk-delete')" @click.prevent="bulkDelete">
|
||||
<i class="fa fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<div v-if="profile.CountPeers === 0">
|
||||
<h4>{{ $t('profile.no-peer.headline') }}</h4>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {onMounted, ref} from "vue";
|
||||
import { profileStore } from "@/stores/profile";
|
||||
import { settingsStore } from "@/stores/settings";
|
||||
import { authStore } from "../stores/auth";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
|
||||
const profile = profileStore()
|
||||
const settings = settingsStore()
|
||||
const auth = authStore()
|
||||
|
||||
const webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||
|
||||
onMounted(async () => {
|
||||
await profile.LoadUser()
|
||||
await auth.LoadWebAuthnCredentials()
|
||||
@@ -37,45 +34,6 @@ async function saveRename(credential) {
|
||||
console.error("Failed to rename credential:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const pwFormData = ref({
|
||||
OldPassword: '',
|
||||
Password: '',
|
||||
PasswordRepeat: '',
|
||||
})
|
||||
|
||||
const passwordWeak = computed(() => {
|
||||
return pwFormData.value.Password && pwFormData.value.Password.length > 0 && pwFormData.value.Password.length < settings.Setting('MinPasswordLength')
|
||||
})
|
||||
|
||||
const passwordChangeAllowed = computed(() => {
|
||||
return pwFormData.value.Password && pwFormData.value.Password.length >= settings.Setting('MinPasswordLength') &&
|
||||
pwFormData.value.Password === pwFormData.value.PasswordRepeat &&
|
||||
pwFormData.value.OldPassword && pwFormData.value.OldPassword.length > 0 && pwFormData.value.OldPassword !== pwFormData.value.Password;
|
||||
})
|
||||
|
||||
const updatePassword = async () => {
|
||||
try {
|
||||
await profile.changePassword(pwFormData.value);
|
||||
|
||||
pwFormData.value.OldPassword = '';
|
||||
pwFormData.value.Password = '';
|
||||
pwFormData.value.PasswordRepeat = '';
|
||||
notify({
|
||||
title: "Password changed!",
|
||||
text: "Your password has been changed successfully.",
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
notify({
|
||||
title: "Failed to update password!",
|
||||
text: e.toString(),
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,45 +43,52 @@ const updatePassword = async () => {
|
||||
|
||||
<p class="lead">{{ $t('settings.abstract') }}</p>
|
||||
|
||||
<div class="card border-secondary p-5 mt-5" v-if="profile.user.Source === 'db'">
|
||||
<h2 class="display-7">{{ $t('settings.password.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.password.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4" for="oldpw">{{ $t('settings.password.current-label') }}</label>
|
||||
<input id="oldpw" v-model="pwFormData.OldPassword" class="form-control" :class="{ 'is-invalid': pwFormData.Password && !pwFormData.OldPassword }" type="password">
|
||||
<div v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||
<div class="card border-secondary p-5" v-if="profile.user.ApiToken">
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.active-description') }}</p>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group has-success">
|
||||
<label class="form-label mt-4" for="newpw">{{ $t('settings.password.new-label') }}</label>
|
||||
<input id="newpw" v-model="pwFormData.Password" class="form-control" :class="{ 'is-invalid': passwordWeak, 'is-valid': pwFormData.Password !== '' && !passwordWeak }" type="password">
|
||||
<div class="invalid-feedback" v-if="passwordWeak">{{ $t('settings.password.weak-label') }}</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4" for="confirmnewpw">{{ $t('settings.password.new-confirm-label') }}</label>
|
||||
<input id="confirmnewpw" v-model="pwFormData.PasswordRepeat" class="form-control" :class="{ 'is-invalid': pwFormData.PasswordRepeat !== ''&& pwFormData.Password !== pwFormData.PasswordRepeat, 'is-valid': pwFormData.PasswordRepeat !== '' && pwFormData.Password === pwFormData.PasswordRepeat && !passwordWeak }" type="password">
|
||||
<div class="invalid-feedback" v-if="pwFormData.PasswordRepeat !== ''&& pwFormData.Password !== pwFormData.PasswordRepeat">{{ $t('settings.password.invalid-confirm-label') }}</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="updatePassword" :disabled="profile.isFetching || !passwordChangeAllowed">
|
||||
<i class="fa-solid fa-floppy-disk"></i> {{ $t('settings.password.change-button-text') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
</div>
|
||||
<div class="card border-secondary p-5" v-else>
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||
<button class="btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,53 +173,4 @@ const updatePassword = async () => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-5" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')">
|
||||
<div class="card border-secondary p-5" v-if="profile.user.ApiToken">
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.active-description') }}</p>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.user-label') }}</label>
|
||||
<input v-model="profile.user.Identifier" class="form-control" :placeholder="$t('settings.api.user-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label mt-4">{{ $t('settings.api.token-label') }}</label>
|
||||
<input v-model="profile.user.ApiToken" class="form-control" :placeholder="$t('settings.api.token-placeholder')" type="text" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<p class="form-label mt-4">{{ $t('settings.api.token-created-label') }} {{profile.user.ApiTokenCreated}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-primary" :title="$t('settings.api.button-disable-title')" @click.prevent="profile.disableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-minus-circle"></i> {{ $t('settings.api.button-disable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<a :href="webBasePath + '/api/v1/doc.html'" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card border-secondary p-5" v-else>
|
||||
<h2 class="display-7">{{ $t('settings.api.headline') }}</h2>
|
||||
<p class="lead">{{ $t('settings.api.abstract') }}</p>
|
||||
<hr class="my-4">
|
||||
<p>{{ $t('settings.api.inactive-description') }}</p>
|
||||
<button class="btn btn-primary" :title="$t('settings.api.button-enable-title')" @click.prevent="profile.enableApi()" :disabled="profile.isFetching">
|
||||
<i class="fa-solid fa-plus-circle"></i> {{ $t('settings.api.button-enable-text') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,77 +1,16 @@
|
||||
<script setup>
|
||||
import {userStore} from "@/stores/users";
|
||||
import {ref, onMounted, computed} from "vue";
|
||||
import {ref,onMounted} from "vue";
|
||||
import UserEditModal from "../components/UserEditModal.vue";
|
||||
import UserViewModal from "../components/UserViewModal.vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
|
||||
const users = userStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const editUserId = ref("")
|
||||
const viewedUserId = ref("")
|
||||
|
||||
const selectAll = ref(false)
|
||||
|
||||
const selectedUsers = computed(() => {
|
||||
return users.All.filter(user => user.IsSelected).map(user => user.Identifier);
|
||||
})
|
||||
|
||||
async function bulkDelete() {
|
||||
if (confirm(t('users.confirm-bulk-delete', {count: selectedUsers.value.length}))) {
|
||||
try {
|
||||
await users.BulkDelete(selectedUsers.value)
|
||||
selectAll.value = false // reset selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkEnable() {
|
||||
try {
|
||||
await users.BulkEnable(selectedUsers.value)
|
||||
selectAll.value = false
|
||||
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDisable() {
|
||||
if (confirm(t('users.confirm-bulk-disable', {count: selectedUsers.value.length}))) {
|
||||
try {
|
||||
await users.BulkDisable(selectedUsers.value)
|
||||
selectAll.value = false
|
||||
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkLock() {
|
||||
if (confirm(t('users.confirm-bulk-lock', {count: selectedUsers.value.length}))) {
|
||||
try {
|
||||
await users.BulkLock(selectedUsers.value)
|
||||
selectAll.value = false
|
||||
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkUnlock() {
|
||||
try {
|
||||
await users.BulkUnlock(selectedUsers.value)
|
||||
selectAll.value = false
|
||||
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||
} catch (e) {
|
||||
// notification is handled in store
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
users.FilteredAndPaged.forEach(user => {
|
||||
user.IsSelected = selectAll.value;
|
||||
@@ -106,15 +45,6 @@ onMounted(() => {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-if="selectedUsers.length > 0">
|
||||
<div class="col-12 text-lg-end">
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-enable')" @click.prevent="bulkEnable"><i class="fa-regular fa-circle-check"></i></a>
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-disable')" @click.prevent="bulkDisable"><i class="fa fa-ban"></i></a>
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-unlock')" @click.prevent="bulkUnlock"><i class="fa-solid fa-lock-open"></i></a>
|
||||
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-lock')" @click.prevent="bulkLock"><i class="fa-solid fa-lock"></i></a>
|
||||
<a class="btn btn-outline-danger btn-sm ms-2" href="#" :title="$t('users.button-bulk-delete')" @click.prevent="bulkDelete"><i class="fa fa-trash-can"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 table-responsive">
|
||||
<div v-if="users.Count===0">
|
||||
<h4>{{ $t('users.no-user.headline') }}</h4>
|
||||
@@ -131,7 +61,7 @@ onMounted(() => {
|
||||
<th scope="col">{{ $t('users.table-heading.email') }}</th>
|
||||
<th scope="col">{{ $t('users.table-heading.firstname') }}</th>
|
||||
<th scope="col">{{ $t('users.table-heading.lastname') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.sources') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.source') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.peers') }}</th>
|
||||
<th class="text-center" scope="col">{{ $t('users.table-heading.admin') }}</th>
|
||||
<th scope="col"></th><!-- Actions -->
|
||||
@@ -150,7 +80,7 @@ onMounted(() => {
|
||||
<td>{{user.Email}}</td>
|
||||
<td>{{user.Firstname}}</td>
|
||||
<td>{{user.Lastname}}</td>
|
||||
<td><span class="badge bg-light me-1" v-for="src in user.AuthSources" :key="src">{{src}}</span></td>
|
||||
<td class="text-center"><span class="badge rounded-pill bg-light">{{user.Source}}</span></td>
|
||||
<td class="text-center">{{user.PeerCount}}</td>
|
||||
<td class="text-center">
|
||||
<span v-if="user.IsAdmin" class="text-danger" :title="$t('users.admin')"><i class="fa fa-check-circle"></i></span>
|
||||
|
||||
74
go.mod
74
go.mod
@@ -5,14 +5,13 @@ go 1.24.0
|
||||
require (
|
||||
github.com/a8m/envsubst v1.4.3
|
||||
github.com/alexedwards/scs/v2 v2.9.0
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-pkgz/routegroup v1.6.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/go-webauthn/webauthn v0.15.0
|
||||
github.com/go-pkgz/routegroup v1.5.3
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-webauthn/webauthn v0.14.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/prometheus-community/pro-bing v0.7.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -22,51 +21,50 @@ require (
|
||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlserver v1.6.3
|
||||
gorm.io/gorm v1.31.1
|
||||
gorm.io/driver/sqlserver v1.6.1
|
||||
gorm.io/gorm v1.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.4 // indirect
|
||||
github.com/go-openapi/spec v0.22.2 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-test/deep v1.1.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/go-webauthn/x v0.1.26 // indirect
|
||||
github.com/go-webauthn/x v0.1.25 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-tpm v0.9.7 // indirect
|
||||
github.com/google/go-tpm v0.9.6 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
@@ -78,32 +76,32 @@ require (
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.8.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.9.5 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.9.3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // 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
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
modernc.org/libc v1.67.1 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.40.1 // indirect
|
||||
modernc.org/sqlite v1.39.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
168
go.sum
168
go.sum
@@ -20,8 +20,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:
|
||||
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.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
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/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
@@ -38,8 +38,8 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
|
||||
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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=
|
||||
@@ -50,8 +50,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
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/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
@@ -62,53 +62,47 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
|
||||
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
|
||||
github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc=
|
||||
github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
|
||||
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
|
||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
|
||||
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
github.com/go-pkgz/routegroup v1.5.3 h1:IvH1KLcQkMap9jucQGBlef3IBloxSAe8USUFvxShFqs=
|
||||
github.com/go-pkgz/routegroup v1.5.3/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
|
||||
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/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/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/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY=
|
||||
github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A=
|
||||
github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs=
|
||||
github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg=
|
||||
github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0=
|
||||
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
|
||||
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
|
||||
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
|
||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
@@ -121,8 +115,8 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
|
||||
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
|
||||
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -130,13 +124,9 @@ 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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -183,16 +173,18 @@ github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHi
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
|
||||
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
|
||||
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
|
||||
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
|
||||
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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/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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/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/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
@@ -205,17 +197,15 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
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=
|
||||
@@ -272,18 +262,18 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
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.9.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -301,10 +291,10 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -312,8 +302,8 @@ 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.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -334,8 +324,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -364,16 +354,16 @@ 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.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
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.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.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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
@@ -395,25 +385,23 @@ gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
|
||||
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
|
||||
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
|
||||
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -422,8 +410,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
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/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -23,6 +23,9 @@ import (
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
||||
var SchemaVersion uint64 = 1
|
||||
|
||||
// SysStat stores the current database schema version and the timestamp when it was applied.
|
||||
type SysStat struct {
|
||||
MigratedAt time.Time `gorm:"column:migrated_at"`
|
||||
@@ -163,9 +166,6 @@ func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
|
||||
}
|
||||
if err := os.Chmod(cfg.DSN, 0600); err != nil {
|
||||
return nil, fmt.Errorf("failed to set permissions on sqlite database: %w", err)
|
||||
}
|
||||
sqlDB, _ := gormDb.DB()
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
}
|
||||
@@ -176,15 +176,13 @@ func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
// SqlRepo is a SQL database repository implementation.
|
||||
// Currently, it supports MySQL, SQLite, Microsoft SQL and Postgresql database systems.
|
||||
type SqlRepo struct {
|
||||
db *gorm.DB
|
||||
cfg *config.Config
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewSqlRepository creates a new SqlRepo instance.
|
||||
func NewSqlRepository(db *gorm.DB, cfg *config.Config) (*SqlRepo, error) {
|
||||
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
||||
repo := &SqlRepo{
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
}
|
||||
|
||||
if err := repo.preCheck(); err != nil {
|
||||
@@ -222,8 +220,6 @@ func (r *SqlRepo) preCheck() error {
|
||||
func (r *SqlRepo) migrate() error {
|
||||
slog.Debug("running migration: sys-stat", "result", r.db.AutoMigrate(&SysStat{}))
|
||||
slog.Debug("running migration: user", "result", r.db.AutoMigrate(&domain.User{}))
|
||||
slog.Debug("running migration: user authentications", "result",
|
||||
r.db.AutoMigrate(&domain.UserAuthentication{}))
|
||||
slog.Debug("running migration: user webauthn credentials", "result",
|
||||
r.db.AutoMigrate(&domain.UserWebauthnCredential{}))
|
||||
slog.Debug("running migration: interface", "result", r.db.AutoMigrate(&domain.Interface{}))
|
||||
@@ -233,88 +229,16 @@ func (r *SqlRepo) migrate() error {
|
||||
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
|
||||
|
||||
existingSysStat := SysStat{}
|
||||
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
|
||||
|
||||
// Migration: 0 --> 1
|
||||
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
||||
if existingSysStat.SchemaVersion == 0 {
|
||||
const schemaVersion = 1
|
||||
sysStat := SysStat{
|
||||
MigratedAt: time.Now(),
|
||||
SchemaVersion: schemaVersion,
|
||||
SchemaVersion: SchemaVersion,
|
||||
}
|
||||
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)
|
||||
}
|
||||
slog.Debug("sys-stat entry written", "schema_version", schemaVersion)
|
||||
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
|
||||
}
|
||||
|
||||
// Migration: 1 --> 2
|
||||
if existingSysStat.SchemaVersion == 1 {
|
||||
const schemaVersion = 2
|
||||
// Preserve existing behavior for installations that had default-peer-creation enabled.
|
||||
if r.cfg.Core.CreateDefaultPeer {
|
||||
err := r.db.Model(&domain.Interface{}).
|
||||
Where("type = ?", domain.InterfaceTypeServer).
|
||||
Update("create_default_peer", true).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate interface flags for schema version %d: %w", schemaVersion, err)
|
||||
}
|
||||
slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion)
|
||||
}
|
||||
sysStat := SysStat{
|
||||
MigratedAt: time.Now(),
|
||||
SchemaVersion: schemaVersion,
|
||||
}
|
||||
if err := r.db.Create(&sysStat).Error; err != nil {
|
||||
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
|
||||
}
|
||||
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
|
||||
}
|
||||
|
||||
// Migration: 2 --> 3
|
||||
if existingSysStat.SchemaVersion == 2 {
|
||||
const schemaVersion = 3
|
||||
// Migration to multi-auth
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var users []domain.User
|
||||
if err := tx.Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, user := range users {
|
||||
auth := domain.UserAuthentication{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: domain.CtxSystemDBMigrator,
|
||||
UpdatedBy: domain.CtxSystemDBMigrator,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: user.Identifier,
|
||||
Source: user.Source,
|
||||
ProviderName: user.ProviderName,
|
||||
}
|
||||
if err := tx.Create(&auth).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("migrated users to multi-auth model", "schema_version", schemaVersion)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate to multi-auth: %w", err)
|
||||
}
|
||||
sysStat := SysStat{
|
||||
MigratedAt: time.Now(),
|
||||
SchemaVersion: schemaVersion,
|
||||
}
|
||||
if err := r.db.Create(&sysStat).Error; err != nil {
|
||||
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", schemaVersion, err)
|
||||
}
|
||||
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
|
||||
slog.Debug("sys-stat entry written", "schema_version", SchemaVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -824,7 +748,7 @@ func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr
|
||||
func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
|
||||
var user domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").Preload("Authentications").First(&user, id).Error
|
||||
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").First(&user, id).Error
|
||||
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrNotFound
|
||||
@@ -842,8 +766,7 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
|
||||
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).Where("email = ?",
|
||||
email).Preload("WebAuthnCredentialList").Preload("Authentications").Find(&users).Error
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrNotFound
|
||||
}
|
||||
@@ -883,7 +806,7 @@ func (r *SqlRepo) GetUserByWebAuthnCredential(ctx context.Context, credentialIdB
|
||||
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
|
||||
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").Preload("Authentications").Find(&users).Error
|
||||
err := r.db.WithContext(ctx).Preload("WebAuthnCredentialList").Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -903,7 +826,6 @@ func (r *SqlRepo) FindUsers(ctx context.Context, search string) ([]domain.User,
|
||||
Or("lastname LIKE ?", searchValue).
|
||||
Or("email LIKE ?", searchValue).
|
||||
Preload("WebAuthnCredentialList").
|
||||
Preload("Authentications").
|
||||
Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -963,17 +885,7 @@ func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id do
|
||||
) {
|
||||
var user domain.User
|
||||
|
||||
result := tx.Model(&user).Preload("WebAuthnCredentialList").Preload("Authentications").Find(&user, id)
|
||||
if result.Error != nil {
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, result.Error
|
||||
}
|
||||
}
|
||||
if result.Error == nil && result.RowsAffected > 0 {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// create a new user record if no user record exists yet
|
||||
// userDefaults will be applied to newly created user records
|
||||
userDefaults := domain.User{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: ui.UserId(),
|
||||
@@ -982,15 +894,16 @@ func (r *SqlRepo) getOrCreateUser(ui *domain.ContextUserInfo, tx *gorm.DB, id do
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Identifier: id,
|
||||
Source: domain.UserSourceDatabase,
|
||||
IsAdmin: false,
|
||||
}
|
||||
|
||||
err := tx.Create(&userDefaults).Error
|
||||
err := tx.Attrs(userDefaults).FirstOrCreate(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &userDefaults, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *domain.User) error {
|
||||
@@ -1007,11 +920,6 @@ func (r *SqlRepo) upsertUser(ui *domain.ContextUserInfo, tx *gorm.DB, user *doma
|
||||
return fmt.Errorf("failed to update users webauthn credentials: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Session(&gorm.Session{FullSaveAssociations: true}).Unscoped().Model(user).Association("Authentications").Unscoped().Replace(user.Authentications)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update users authentications: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -626,7 +626,7 @@ func (c LocalController) exec(command string, interfaceId domain.InterfaceIdenti
|
||||
if err != nil {
|
||||
slog.Warn("failed to executed shell command",
|
||||
"command", commandWithInterfaceName, "stdin", stdin, "output", string(out), "error", err)
|
||||
return fmt.Errorf("failed to execute shell command %s: %w", commandWithInterfaceName, err)
|
||||
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
||||
}
|
||||
slog.Debug("executed shell command",
|
||||
"command", commandWithInterfaceName,
|
||||
|
||||
@@ -1,979 +0,0 @@
|
||||
package wgcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
"github.com/h44z/wg-portal/internal/lowlevel"
|
||||
)
|
||||
|
||||
// PfsenseController implements the InterfaceController interface for pfSense firewalls.
|
||||
// It uses the pfSense REST API (https://pfrest.org/) to manage WireGuard interfaces and peers.
|
||||
// API endpoint paths and field names should be verified against the Swagger documentation:
|
||||
// https://pfrest.org/api-docs/
|
||||
|
||||
type PfsenseController struct {
|
||||
coreCfg *config.Config
|
||||
cfg *config.BackendPfsense
|
||||
|
||||
client *lowlevel.PfsenseApiClient
|
||||
|
||||
// Add mutexes to prevent race conditions
|
||||
interfaceMutexes sync.Map // map[domain.InterfaceIdentifier]*sync.Mutex
|
||||
peerMutexes sync.Map // map[domain.PeerIdentifier]*sync.Mutex
|
||||
coreMutex sync.Mutex // for updating the core configuration such as routing table or DNS settings
|
||||
}
|
||||
|
||||
func NewPfsenseController(coreCfg *config.Config, cfg *config.BackendPfsense) (*PfsenseController, error) {
|
||||
client, err := lowlevel.NewPfsenseApiClient(coreCfg, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pfSense API client: %w", err)
|
||||
}
|
||||
|
||||
return &PfsenseController{
|
||||
coreCfg: coreCfg,
|
||||
cfg: cfg,
|
||||
|
||||
client: client,
|
||||
|
||||
interfaceMutexes: sync.Map{},
|
||||
peerMutexes: sync.Map{},
|
||||
coreMutex: sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) GetId() domain.InterfaceBackend {
|
||||
return domain.InterfaceBackend(c.cfg.Id)
|
||||
}
|
||||
|
||||
// getInterfaceMutex returns a mutex for the given interface to prevent concurrent modifications
|
||||
func (c *PfsenseController) getInterfaceMutex(id domain.InterfaceIdentifier) *sync.Mutex {
|
||||
mutex, _ := c.interfaceMutexes.LoadOrStore(id, &sync.Mutex{})
|
||||
return mutex.(*sync.Mutex)
|
||||
}
|
||||
|
||||
// getPeerMutex returns a mutex for the given peer to prevent concurrent modifications
|
||||
func (c *PfsenseController) getPeerMutex(id domain.PeerIdentifier) *sync.Mutex {
|
||||
mutex, _ := c.peerMutexes.LoadOrStore(id, &sync.Mutex{})
|
||||
return mutex.(*sync.Mutex)
|
||||
}
|
||||
|
||||
// region wireguard-related
|
||||
|
||||
func (c *PfsenseController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
|
||||
// Query WireGuard tunnels from pfSense API
|
||||
// Using pfSense REST API v2 endpoints: GET /api/v2/vpn/wireguard/tunnels
|
||||
// Field names should be verified against Swagger docs: https://pfrest.org/api-docs/
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/tunnels", &lowlevel.PfsenseRequestOptions{})
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return nil, fmt.Errorf("failed to query interfaces: %v", wgReply.Error)
|
||||
}
|
||||
|
||||
// Parallelize loading of interface details to speed up overall latency.
|
||||
// Use a bounded semaphore to avoid overloading the pfSense device.
|
||||
maxConcurrent := c.cfg.GetConcurrency()
|
||||
sem := make(chan struct{}, maxConcurrent)
|
||||
|
||||
interfaces := make([]domain.PhysicalInterface, 0, len(wgReply.Data))
|
||||
var mu sync.Mutex
|
||||
var wgWait sync.WaitGroup
|
||||
var firstErr error
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
for _, wgObj := range wgReply.Data {
|
||||
wgWait.Add(1)
|
||||
sem <- struct{}{} // block if more than maxConcurrent requests are processing
|
||||
go func(wg lowlevel.GenericJsonObject) {
|
||||
defer wgWait.Done()
|
||||
defer func() { <-sem }() // read from the semaphore and make space for the next entry
|
||||
if firstErr != nil {
|
||||
return
|
||||
}
|
||||
pi, err := c.loadInterfaceData(ctx2, wg)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
cancel()
|
||||
}
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
interfaces = append(interfaces, *pi)
|
||||
mu.Unlock()
|
||||
}(wgObj)
|
||||
}
|
||||
|
||||
wgWait.Wait()
|
||||
if firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (
|
||||
*domain.PhysicalInterface,
|
||||
error,
|
||||
) {
|
||||
// First, get the tunnel ID by querying by name
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/tunnels", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"name": string(id),
|
||||
},
|
||||
})
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
|
||||
}
|
||||
|
||||
if len(wgReply.Data) == 0 {
|
||||
return nil, fmt.Errorf("interface %s not found", id)
|
||||
}
|
||||
|
||||
tunnelId := wgReply.Data[0].GetString("id")
|
||||
|
||||
// Query the specific tunnel endpoint to get full details including addresses
|
||||
// Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id}
|
||||
if tunnelId != "" {
|
||||
tunnelReply := c.client.Get(ctx, "/api/v2/vpn/wireguard/tunnel", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"id": tunnelId,
|
||||
},
|
||||
})
|
||||
if tunnelReply.Status == lowlevel.PfsenseApiStatusOk && tunnelReply.Data != nil {
|
||||
// Use the detailed tunnel response which includes addresses
|
||||
return c.loadInterfaceData(ctx, tunnelReply.Data)
|
||||
}
|
||||
// Fall back to list response if detail query fails
|
||||
if c.cfg.Debug {
|
||||
slog.Debug("failed to query detailed tunnel info, using list response", "interface", id, "tunnel_id", tunnelId)
|
||||
}
|
||||
}
|
||||
|
||||
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||
}
|
||||
|
||||
func (c *PfsenseController) loadInterfaceData(
|
||||
ctx context.Context,
|
||||
wireGuardObj lowlevel.GenericJsonObject,
|
||||
) (*domain.PhysicalInterface, error) {
|
||||
deviceName := wireGuardObj.GetString("name")
|
||||
deviceId := wireGuardObj.GetString("id")
|
||||
|
||||
// Extract addresses from the tunnel data
|
||||
// The tunnel response may include an "addresses" array when queried via /tunnel?id={id}
|
||||
addresses := c.extractAddresses(wireGuardObj, nil)
|
||||
|
||||
// If addresses weren't found in the tunnel object and we have a tunnel ID,
|
||||
// query the specific tunnel endpoint to get full details including addresses
|
||||
// Endpoint: GET /api/v2/vpn/wireguard/tunnel?id={id}
|
||||
if len(addresses) == 0 && deviceId != "" {
|
||||
tunnelReply := c.client.Get(ctx, "/api/v2/vpn/wireguard/tunnel", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"id": deviceId,
|
||||
},
|
||||
})
|
||||
if tunnelReply.Status == lowlevel.PfsenseApiStatusOk && tunnelReply.Data != nil {
|
||||
// Extract addresses from the detailed tunnel response
|
||||
parsedAddrs := c.extractAddresses(tunnelReply.Data, nil)
|
||||
if len(parsedAddrs) > 0 {
|
||||
addresses = parsedAddrs
|
||||
if c.cfg.Debug {
|
||||
slog.Debug("loaded addresses from detailed tunnel query", "interface", deviceName, "count", len(addresses))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interfaceModel, err := c.convertWireGuardInterface(wireGuardObj, nil, addresses)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("interface convert failed for %s: %w", deviceName, err)
|
||||
}
|
||||
return &interfaceModel, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) extractAddresses(
|
||||
wgObj lowlevel.GenericJsonObject,
|
||||
ifaceObj lowlevel.GenericJsonObject,
|
||||
) []domain.Cidr {
|
||||
addresses := make([]domain.Cidr, 0)
|
||||
|
||||
// Try to get addresses from ifaceObj first
|
||||
if ifaceObj != nil {
|
||||
addrStr := ifaceObj.GetString("addresses")
|
||||
if addrStr != "" {
|
||||
// Addresses might be comma-separated or in an array
|
||||
addrs, _ := domain.CidrsFromString(addrStr)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get addresses from wgObj - check if it's an array first
|
||||
if len(addresses) == 0 {
|
||||
if addressesValue, ok := wgObj["addresses"]; ok && addressesValue != nil {
|
||||
if addressesArray, ok := addressesValue.([]any); ok {
|
||||
// Parse addresses array (from /tunnel?id={id} response)
|
||||
// Each object has "address" and "mask" fields
|
||||
for _, addrItem := range addressesArray {
|
||||
if addrObj, ok := addrItem.(map[string]any); ok {
|
||||
address := ""
|
||||
mask := 0
|
||||
|
||||
// Extract address
|
||||
if addrVal, ok := addrObj["address"]; ok {
|
||||
if addrStr, ok := addrVal.(string); ok {
|
||||
address = addrStr
|
||||
} else {
|
||||
address = fmt.Sprintf("%v", addrVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract mask
|
||||
if maskVal, ok := addrObj["mask"]; ok {
|
||||
if maskInt, ok := maskVal.(int); ok {
|
||||
mask = maskInt
|
||||
} else if maskFloat, ok := maskVal.(float64); ok {
|
||||
mask = int(maskFloat)
|
||||
} else if maskStr, ok := maskVal.(string); ok {
|
||||
if maskInt, err := strconv.Atoi(maskStr); err == nil {
|
||||
mask = maskInt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to CIDR format
|
||||
if address != "" && mask > 0 {
|
||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||
if cidr, err := domain.CidrFromString(cidrStr); err == nil {
|
||||
addresses = append(addresses, cidr)
|
||||
}
|
||||
} else if address != "" {
|
||||
// Try parsing as CIDR string directly
|
||||
if cidr, err := domain.CidrFromString(address); err == nil {
|
||||
addresses = append(addresses, cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if addrStr, ok := addressesValue.(string); ok {
|
||||
// Fallback: try parsing as comma-separated string
|
||||
addrs, _ := domain.CidrsFromString(addrStr)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
} else {
|
||||
// Try as string field
|
||||
addrStr := wgObj.GetString("addresses")
|
||||
if addrStr != "" {
|
||||
addrs, _ := domain.CidrsFromString(addrStr)
|
||||
addresses = append(addresses, addrs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
// parseAddressArray parses an array of address objects from the pfSense API
|
||||
// Each object has "address" and "mask" fields (similar to allowedips structure)
|
||||
func (c *PfsenseController) parseAddressArray(addressArray []lowlevel.GenericJsonObject) []domain.Cidr {
|
||||
addresses := make([]domain.Cidr, 0, len(addressArray))
|
||||
|
||||
for _, addrObj := range addressArray {
|
||||
address := addrObj.GetString("address")
|
||||
mask := addrObj.GetInt("mask")
|
||||
|
||||
if address != "" && mask > 0 {
|
||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||
if cidr, err := domain.CidrFromString(cidrStr); err == nil {
|
||||
addresses = append(addresses, cidr)
|
||||
}
|
||||
} else if address != "" {
|
||||
// Try parsing as CIDR string directly
|
||||
if cidr, err := domain.CidrFromString(address); err == nil {
|
||||
addresses = append(addresses, cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
func (c *PfsenseController) convertWireGuardInterface(
|
||||
wg, iface lowlevel.GenericJsonObject,
|
||||
addresses []domain.Cidr,
|
||||
) (
|
||||
domain.PhysicalInterface,
|
||||
error,
|
||||
) {
|
||||
// Map pfSense field names to our domain model
|
||||
// Field names should be verified against the Swagger UI: https://pfrest.org/api-docs/
|
||||
// The implementation attempts to handle both camelCase and kebab-case variations
|
||||
privateKey := wg.GetString("privatekey")
|
||||
if privateKey == "" {
|
||||
privateKey = wg.GetString("private-key")
|
||||
}
|
||||
publicKey := wg.GetString("publickey")
|
||||
if publicKey == "" {
|
||||
publicKey = wg.GetString("public-key")
|
||||
}
|
||||
|
||||
listenPort := wg.GetInt("listenport")
|
||||
if listenPort == 0 {
|
||||
listenPort = wg.GetInt("listen-port")
|
||||
}
|
||||
|
||||
mtu := wg.GetInt("mtu")
|
||||
running := wg.GetBool("running")
|
||||
disabled := wg.GetBool("disabled")
|
||||
|
||||
// TODO: Interface statistics (rx/tx bytes) are not currently supported
|
||||
// by the pfSense REST API. This functionality is reserved for future implementation.
|
||||
var rxBytes, txBytes uint64
|
||||
|
||||
pi := domain.PhysicalInterface{
|
||||
Identifier: domain.InterfaceIdentifier(wg.GetString("name")),
|
||||
KeyPair: domain.KeyPair{
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: publicKey,
|
||||
},
|
||||
ListenPort: listenPort,
|
||||
Addresses: addresses,
|
||||
Mtu: mtu,
|
||||
FirewallMark: 0,
|
||||
DeviceUp: running && !disabled,
|
||||
ImportSource: domain.ControllerTypePfsense,
|
||||
DeviceType: domain.ControllerTypePfsense,
|
||||
BytesUpload: txBytes,
|
||||
BytesDownload: rxBytes,
|
||||
}
|
||||
|
||||
// Extract description - pfSense API uses "descr" field
|
||||
description := wg.GetString("descr")
|
||||
if description == "" {
|
||||
description = wg.GetString("description")
|
||||
}
|
||||
if description == "" {
|
||||
description = wg.GetString("comment")
|
||||
}
|
||||
|
||||
pi.SetExtras(domain.PfsenseInterfaceExtras{
|
||||
Id: wg.GetString("id"),
|
||||
Comment: description,
|
||||
Disabled: disabled,
|
||||
})
|
||||
|
||||
return pi, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) GetPeers(ctx context.Context, deviceId domain.InterfaceIdentifier) (
|
||||
[]domain.PhysicalPeer,
|
||||
error,
|
||||
) {
|
||||
// Query all peers and filter by interface client-side
|
||||
// Using pfSense REST API v2 endpoints (https://pfrest.org/)
|
||||
// The API uses query parameters like ?id=0 for specific items, but we need to filter
|
||||
// by interface (tun field), so we fetch all peers and filter client-side
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{})
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return nil, fmt.Errorf("failed to query peers for %s: %v", deviceId, wgReply.Error)
|
||||
}
|
||||
|
||||
if len(wgReply.Data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Filter peers client-side by checking the "tun" field in each peer
|
||||
// pfSense peer responses use "tun" field to indicate which tunnel/interface the peer belongs to
|
||||
peers := make([]domain.PhysicalPeer, 0, len(wgReply.Data))
|
||||
for _, peer := range wgReply.Data {
|
||||
// Check if this peer belongs to the requested interface
|
||||
// pfSense uses "tun" field with the interface name (e.g., "tun_wg0")
|
||||
peerTun := peer.GetString("tun")
|
||||
if peerTun == "" {
|
||||
// Try alternative field names as fallback
|
||||
peerTun = peer.GetString("interface")
|
||||
if peerTun == "" {
|
||||
peerTun = peer.GetString("tunnel")
|
||||
}
|
||||
}
|
||||
|
||||
// Only include peers that match the requested interface name
|
||||
if peerTun != string(deviceId) {
|
||||
if c.cfg.Debug {
|
||||
slog.Debug("skipping peer - interface mismatch",
|
||||
"peer", peer.GetString("name"),
|
||||
"peer_tun", peerTun,
|
||||
"requested_interface", deviceId,
|
||||
"peer_id", peer.GetString("id"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Use peer data directly from the list response
|
||||
peerModel, err := c.convertWireGuardPeer(peer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer convert failed for %v: %w", peer.GetString("name"), err)
|
||||
}
|
||||
peers = append(peers, peerModel)
|
||||
}
|
||||
|
||||
if c.cfg.Debug {
|
||||
slog.Debug("filtered peers for interface",
|
||||
"interface", deviceId,
|
||||
"total_peers_from_api", len(wgReply.Data),
|
||||
"filtered_peers", len(peers))
|
||||
}
|
||||
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (
|
||||
domain.PhysicalPeer,
|
||||
error,
|
||||
) {
|
||||
publicKey := peer.GetString("publickey")
|
||||
if publicKey == "" {
|
||||
publicKey = peer.GetString("public-key")
|
||||
}
|
||||
|
||||
privateKey := peer.GetString("privatekey")
|
||||
if privateKey == "" {
|
||||
privateKey = peer.GetString("private-key")
|
||||
}
|
||||
|
||||
presharedKey := peer.GetString("presharedkey")
|
||||
if presharedKey == "" {
|
||||
presharedKey = peer.GetString("preshared-key")
|
||||
}
|
||||
|
||||
// pfSense returns allowedips as an array of objects with "address" and "mask" fields
|
||||
// Example: [{"address": "10.1.2.3", "mask": 32, ...}, ...]
|
||||
var allowedAddresses []domain.Cidr
|
||||
if allowedIPsValue, ok := peer["allowedips"]; ok {
|
||||
if allowedIPsArray, ok := allowedIPsValue.([]any); ok {
|
||||
// Parse array of objects
|
||||
for _, item := range allowedIPsArray {
|
||||
if itemObj, ok := item.(map[string]any); ok {
|
||||
address := ""
|
||||
mask := 0
|
||||
|
||||
// Extract address
|
||||
if addrVal, ok := itemObj["address"]; ok {
|
||||
if addrStr, ok := addrVal.(string); ok {
|
||||
address = addrStr
|
||||
} else {
|
||||
address = fmt.Sprintf("%v", addrVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract mask
|
||||
if maskVal, ok := itemObj["mask"]; ok {
|
||||
if maskInt, ok := maskVal.(int); ok {
|
||||
mask = maskInt
|
||||
} else if maskFloat, ok := maskVal.(float64); ok {
|
||||
mask = int(maskFloat)
|
||||
} else if maskStr, ok := maskVal.(string); ok {
|
||||
if maskInt, err := strconv.Atoi(maskStr); err == nil {
|
||||
mask = maskInt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to CIDR format (e.g., "10.1.2.3/32")
|
||||
if address != "" && mask > 0 {
|
||||
cidrStr := fmt.Sprintf("%s/%d", address, mask)
|
||||
if cidr, err := domain.CidrFromString(cidrStr); err == nil {
|
||||
allowedAddresses = append(allowedAddresses, cidr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if allowedIPsStr, ok := allowedIPsValue.(string); ok {
|
||||
// Fallback: try parsing as comma-separated string
|
||||
allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to string parsing if array parsing didn't work
|
||||
if len(allowedAddresses) == 0 {
|
||||
allowedIPsStr := peer.GetString("allowedips")
|
||||
if allowedIPsStr == "" {
|
||||
allowedIPsStr = peer.GetString("allowed-ips")
|
||||
}
|
||||
if allowedIPsStr != "" {
|
||||
allowedAddresses, _ = domain.CidrsFromString(allowedIPsStr)
|
||||
}
|
||||
}
|
||||
|
||||
endpoint := peer.GetString("endpoint")
|
||||
port := peer.GetString("port")
|
||||
|
||||
// Combine endpoint and port if both are available
|
||||
if endpoint != "" && port != "" {
|
||||
// Check if endpoint already contains a port
|
||||
if !strings.Contains(endpoint, ":") {
|
||||
endpoint = fmt.Sprintf("%s:%s", endpoint, port)
|
||||
}
|
||||
} else if endpoint == "" && port != "" {
|
||||
// If only port is available, we can't construct a full endpoint
|
||||
// This might be used with the interface's listenport
|
||||
}
|
||||
|
||||
keepAliveSeconds := 0
|
||||
keepAliveStr := peer.GetString("persistentkeepalive")
|
||||
if keepAliveStr == "" {
|
||||
keepAliveStr = peer.GetString("persistent-keepalive")
|
||||
}
|
||||
if keepAliveStr != "" {
|
||||
duration, err := time.ParseDuration(keepAliveStr)
|
||||
if err == nil {
|
||||
keepAliveSeconds = int(duration.Seconds())
|
||||
} else {
|
||||
// Try parsing as integer (seconds)
|
||||
if secs, err := strconv.Atoi(keepAliveStr); err == nil {
|
||||
keepAliveSeconds = secs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Peer statistics (last handshake, rx/tx bytes) are not currently supported
|
||||
// by the pfSense REST API. This functionality is reserved for future implementation
|
||||
// when the API adds support for these fields.
|
||||
// See: https://github.com/jaredhendrickson13/pfsense-api/issues (issue opened by user)
|
||||
//
|
||||
// When supported, extract fields like:
|
||||
// - lastHandshake: peer.GetString("lasthandshake") or peer.GetString("last-handshake")
|
||||
// - rxBytes: peer.GetInt("rxbytes") or peer.GetInt("rx-bytes")
|
||||
// - txBytes: peer.GetInt("txbytes") or peer.GetInt("tx-bytes")
|
||||
lastHandshakeTime := time.Time{}
|
||||
rxBytes := uint64(0)
|
||||
txBytes := uint64(0)
|
||||
|
||||
peerModel := domain.PhysicalPeer{
|
||||
Identifier: domain.PeerIdentifier(publicKey),
|
||||
Endpoint: endpoint,
|
||||
AllowedIPs: allowedAddresses,
|
||||
KeyPair: domain.KeyPair{
|
||||
PublicKey: publicKey,
|
||||
PrivateKey: privateKey,
|
||||
},
|
||||
PresharedKey: domain.PreSharedKey(presharedKey),
|
||||
PersistentKeepalive: keepAliveSeconds,
|
||||
LastHandshake: lastHandshakeTime,
|
||||
ProtocolVersion: 0, // pfSense may not expose protocol version
|
||||
BytesUpload: txBytes,
|
||||
BytesDownload: rxBytes,
|
||||
ImportSource: domain.ControllerTypePfsense,
|
||||
}
|
||||
|
||||
// Extract description/name - pfSense API uses "descr" field
|
||||
description := peer.GetString("descr")
|
||||
if description == "" {
|
||||
description = peer.GetString("description")
|
||||
}
|
||||
if description == "" {
|
||||
description = peer.GetString("comment")
|
||||
}
|
||||
|
||||
// Extract name - pfSense API may use "name" or "descr"
|
||||
name := peer.GetString("name")
|
||||
if name == "" {
|
||||
name = peer.GetString("descr")
|
||||
}
|
||||
if name == "" {
|
||||
name = description // fallback to description if name is not available
|
||||
}
|
||||
|
||||
peerModel.SetExtras(domain.PfsensePeerExtras{
|
||||
Id: peer.GetString("id"),
|
||||
Name: name,
|
||||
Comment: description,
|
||||
Disabled: peer.GetBool("disabled"),
|
||||
ClientEndpoint: "", // pfSense may handle this differently
|
||||
ClientAddress: "", // pfSense may handle this differently
|
||||
ClientDns: "", // pfSense may handle this differently
|
||||
ClientKeepalive: 0, // pfSense may handle this differently
|
||||
})
|
||||
|
||||
return peerModel, nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) SaveInterface(
|
||||
ctx context.Context,
|
||||
id domain.InterfaceIdentifier,
|
||||
updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
|
||||
) error {
|
||||
// Lock the interface to prevent concurrent modifications
|
||||
mutex := c.getInterfaceMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
physicalInterface, err := c.getOrCreateInterface(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceId := ""
|
||||
if physicalInterface.GetExtras() != nil {
|
||||
if extras, ok := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras); ok {
|
||||
deviceId = extras.Id
|
||||
}
|
||||
}
|
||||
|
||||
if updateFunc != nil {
|
||||
physicalInterface, err = updateFunc(physicalInterface)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if deviceId != "" {
|
||||
// Ensure the ID is preserved
|
||||
if extras, ok := physicalInterface.GetExtras().(domain.PfsenseInterfaceExtras); ok {
|
||||
extras.Id = deviceId
|
||||
physicalInterface.SetExtras(extras)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.updateInterface(ctx, physicalInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) getOrCreateInterface(
|
||||
ctx context.Context,
|
||||
id domain.InterfaceIdentifier,
|
||||
) (*domain.PhysicalInterface, error) {
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/tunnels", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"name": string(id),
|
||||
},
|
||||
})
|
||||
if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
|
||||
return c.loadInterfaceData(ctx, wgReply.Data[0])
|
||||
}
|
||||
|
||||
// create a new tunnel if it does not exist
|
||||
// Actual endpoint: POST /api/v2/vpn/wireguard/tunnel (singular)
|
||||
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/tunnel", lowlevel.GenericJsonObject{
|
||||
"name": string(id),
|
||||
})
|
||||
if createReply.Status == lowlevel.PfsenseApiStatusOk {
|
||||
return c.loadInterfaceData(ctx, createReply.Data)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to create interface %s: %v", id, createReply.Error)
|
||||
}
|
||||
|
||||
func (c *PfsenseController) updateInterface(ctx context.Context, pi *domain.PhysicalInterface) error {
|
||||
extras := pi.GetExtras().(domain.PfsenseInterfaceExtras)
|
||||
interfaceId := extras.Id
|
||||
|
||||
payload := lowlevel.GenericJsonObject{
|
||||
"name": string(pi.Identifier),
|
||||
"description": extras.Comment,
|
||||
"mtu": strconv.Itoa(pi.Mtu),
|
||||
"listenport": strconv.Itoa(pi.ListenPort),
|
||||
"privatekey": pi.KeyPair.PrivateKey,
|
||||
"disabled": strconv.FormatBool(!pi.DeviceUp),
|
||||
}
|
||||
|
||||
// Add addresses if present
|
||||
if len(pi.Addresses) > 0 {
|
||||
addresses := make([]string, 0, len(pi.Addresses))
|
||||
for _, addr := range pi.Addresses {
|
||||
addresses = append(addresses, addr.String())
|
||||
}
|
||||
payload["addresses"] = strings.Join(addresses, ",")
|
||||
}
|
||||
|
||||
// Actual endpoint: PATCH /api/v2/vpn/wireguard/tunnel?id={id}
|
||||
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId, payload)
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("failed to update interface %s: %v", pi.Identifier, wgReply.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error {
|
||||
// Lock the interface to prevent concurrent modifications
|
||||
mutex := c.getInterfaceMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// Find the tunnel ID
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/tunnels", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"name": string(id),
|
||||
},
|
||||
})
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("unable to find WireGuard tunnel %s: %v", id, wgReply.Error)
|
||||
}
|
||||
if len(wgReply.Data) == 0 {
|
||||
return nil // tunnel does not exist, nothing to delete
|
||||
}
|
||||
|
||||
interfaceId := wgReply.Data[0].GetString("id")
|
||||
// Actual endpoint: DELETE /api/v2/vpn/wireguard/tunnel?id={id}
|
||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/tunnel?id="+interfaceId)
|
||||
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("failed to delete WireGuard interface %s: %v", id, deleteReply.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) SavePeer(
|
||||
ctx context.Context,
|
||||
deviceId domain.InterfaceIdentifier,
|
||||
id domain.PeerIdentifier,
|
||||
updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
|
||||
) error {
|
||||
// Lock the peer to prevent concurrent modifications
|
||||
mutex := c.getPeerMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
physicalPeer, err := c.getOrCreatePeer(ctx, deviceId, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerId := ""
|
||||
if physicalPeer.GetExtras() != nil {
|
||||
if extras, ok := physicalPeer.GetExtras().(domain.PfsensePeerExtras); ok {
|
||||
peerId = extras.Id
|
||||
}
|
||||
}
|
||||
|
||||
physicalPeer, err = updateFunc(physicalPeer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if peerId != "" {
|
||||
// Ensure the ID is preserved
|
||||
if extras, ok := physicalPeer.GetExtras().(domain.PfsensePeerExtras); ok {
|
||||
extras.Id = peerId
|
||||
physicalPeer.SetExtras(extras)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.updatePeer(ctx, deviceId, physicalPeer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) getOrCreatePeer(
|
||||
ctx context.Context,
|
||||
deviceId domain.InterfaceIdentifier,
|
||||
id domain.PeerIdentifier,
|
||||
) (*domain.PhysicalPeer, error) {
|
||||
// Query for peer by publickey and interface (tun field)
|
||||
// The API uses query parameters like ?publickey=...&tun=...
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"publickey": string(id),
|
||||
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
||||
},
|
||||
})
|
||||
if wgReply.Status == lowlevel.PfsenseApiStatusOk && len(wgReply.Data) > 0 {
|
||||
slog.Debug("found existing pfSense peer", "peer", id, "interface", deviceId)
|
||||
existingPeer, err := c.convertWireGuardPeer(wgReply.Data[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &existingPeer, nil
|
||||
}
|
||||
|
||||
// create a new peer if it does not exist
|
||||
// Actual endpoint: POST /api/v2/vpn/wireguard/peer (singular)
|
||||
slog.Debug("creating new pfSense peer", "peer", id, "interface", deviceId)
|
||||
createReply := c.client.Create(ctx, "/api/v2/vpn/wireguard/peer", lowlevel.GenericJsonObject{
|
||||
"name": fmt.Sprintf("wg-%s", id[0:8]),
|
||||
"interface": string(deviceId),
|
||||
"publickey": string(id),
|
||||
"allowedips": "0.0.0.0/0", // Use 0.0.0.0/0 as default, will be updated by updatePeer
|
||||
})
|
||||
if createReply.Status == lowlevel.PfsenseApiStatusOk {
|
||||
newPeer, err := c.convertWireGuardPeer(createReply.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slog.Debug("successfully created pfSense peer", "peer", id, "interface", deviceId)
|
||||
return &newPeer, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to create peer %s for interface %s: %v", id, deviceId, createReply.Error)
|
||||
}
|
||||
|
||||
func (c *PfsenseController) updatePeer(
|
||||
ctx context.Context,
|
||||
deviceId domain.InterfaceIdentifier,
|
||||
pp *domain.PhysicalPeer,
|
||||
) error {
|
||||
extras := pp.GetExtras().(domain.PfsensePeerExtras)
|
||||
peerId := extras.Id
|
||||
|
||||
allowedIPsStr := domain.CidrsToString(pp.AllowedIPs)
|
||||
|
||||
slog.Debug("updating pfSense peer",
|
||||
"peer", pp.Identifier,
|
||||
"interface", deviceId,
|
||||
"allowed-ips", allowedIPsStr,
|
||||
"allowed-ips-count", len(pp.AllowedIPs),
|
||||
"disabled", extras.Disabled)
|
||||
|
||||
payload := lowlevel.GenericJsonObject{
|
||||
"name": extras.Name,
|
||||
"description": extras.Comment,
|
||||
"presharedkey": string(pp.PresharedKey),
|
||||
"publickey": pp.KeyPair.PublicKey,
|
||||
"privatekey": pp.KeyPair.PrivateKey,
|
||||
"persistentkeepalive": strconv.Itoa(pp.PersistentKeepalive),
|
||||
"disabled": strconv.FormatBool(extras.Disabled),
|
||||
"allowedips": allowedIPsStr,
|
||||
}
|
||||
|
||||
if pp.Endpoint != "" {
|
||||
payload["endpoint"] = pp.Endpoint
|
||||
}
|
||||
|
||||
// Actual endpoint: PATCH /api/v2/vpn/wireguard/peer?id={id}
|
||||
wgReply := c.client.Update(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId, payload)
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("failed to update peer %s on interface %s: %v", pp.Identifier, deviceId, wgReply.Error)
|
||||
}
|
||||
|
||||
if extras.Disabled {
|
||||
slog.Debug("successfully disabled pfSense peer", "peer", pp.Identifier, "interface", deviceId)
|
||||
} else {
|
||||
slog.Debug("successfully updated pfSense peer", "peer", pp.Identifier, "interface", deviceId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) DeletePeer(
|
||||
ctx context.Context,
|
||||
deviceId domain.InterfaceIdentifier,
|
||||
id domain.PeerIdentifier,
|
||||
) error {
|
||||
// Lock the peer to prevent concurrent modifications
|
||||
mutex := c.getPeerMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// Query for peer by publickey and interface (tun field)
|
||||
// The API uses query parameters like ?publickey=...&tun=...
|
||||
wgReply := c.client.Query(ctx, "/api/v2/vpn/wireguard/peers", &lowlevel.PfsenseRequestOptions{
|
||||
Filters: map[string]string{
|
||||
"publickey": string(id),
|
||||
"tun": string(deviceId), // Use "tun" field name as that's what the API uses
|
||||
},
|
||||
})
|
||||
if wgReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("unable to find WireGuard peer %s for interface %s: %v", id, deviceId, wgReply.Error)
|
||||
}
|
||||
if len(wgReply.Data) == 0 {
|
||||
return nil // peer does not exist, nothing to delete
|
||||
}
|
||||
|
||||
peerId := wgReply.Data[0].GetString("id")
|
||||
// Actual endpoint: DELETE /api/v2/vpn/wireguard/peer?id={id}
|
||||
deleteReply := c.client.Delete(ctx, "/api/v2/vpn/wireguard/peer?id="+peerId)
|
||||
if deleteReply.Status != lowlevel.PfsenseApiStatusOk {
|
||||
return fmt.Errorf("failed to delete WireGuard peer %s for interface %s: %v", id, deviceId, deleteReply.Error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion wireguard-related
|
||||
|
||||
// region wg-quick-related
|
||||
|
||||
func (c *PfsenseController) ExecuteInterfaceHook(
|
||||
_ context.Context,
|
||||
_ domain.InterfaceIdentifier,
|
||||
_ string,
|
||||
) error {
|
||||
// TODO implement me
|
||||
slog.Error("interface hooks are not yet supported for pfSense backends, please open an issue on GitHub")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) SetDNS(
|
||||
ctx context.Context,
|
||||
_ domain.InterfaceIdentifier,
|
||||
dnsStr, _ string,
|
||||
) error {
|
||||
// Lock the interface to prevent concurrent modifications
|
||||
c.coreMutex.Lock()
|
||||
defer c.coreMutex.Unlock()
|
||||
|
||||
// pfSense DNS configuration is typically managed at the system level
|
||||
// This may need to be implemented based on pfSense API capabilities
|
||||
slog.Warn("DNS setting is not yet fully supported for pfSense backends")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) UnsetDNS(
|
||||
ctx context.Context,
|
||||
_ domain.InterfaceIdentifier,
|
||||
dnsStr, _ string,
|
||||
) error {
|
||||
// Lock the interface to prevent concurrent modifications
|
||||
c.coreMutex.Lock()
|
||||
defer c.coreMutex.Unlock()
|
||||
|
||||
// pfSense DNS configuration is typically managed at the system level
|
||||
slog.Warn("DNS unsetting is not yet fully supported for pfSense backends")
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion wg-quick-related
|
||||
|
||||
// region routing-related
|
||||
|
||||
func (c *PfsenseController) SetRoutes(_ context.Context, info domain.RoutingTableInfo) error {
|
||||
// pfSense routing is typically managed through the firewall rules and routing tables
|
||||
// This may need to be implemented based on pfSense API capabilities
|
||||
slog.Warn("route setting is not yet fully supported for pfSense backends")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PfsenseController) RemoveRoutes(_ context.Context, info domain.RoutingTableInfo) error {
|
||||
// pfSense routing is typically managed through the firewall rules and routing tables
|
||||
slog.Warn("route removal is not yet fully supported for pfSense backends")
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion routing-related
|
||||
|
||||
// region statistics-related
|
||||
|
||||
func (c *PfsenseController) PingAddresses(
|
||||
ctx context.Context,
|
||||
addr string,
|
||||
) (*domain.PingerResult, error) {
|
||||
// Use pfSense API to ping if available, otherwise return error
|
||||
// This may need to be implemented based on pfSense API capabilities
|
||||
return nil, fmt.Errorf("ping functionality is not yet implemented for pfSense backends")
|
||||
}
|
||||
|
||||
// endregion statistics-related
|
||||
|
||||
@@ -800,126 +800,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/bulk-delete": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Bulk delete selected peers.",
|
||||
"operationId": "peers_handleBulkDelete",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of peer identifiers to delete",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if deletion was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/bulk-disable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Bulk disable selected peers.",
|
||||
"operationId": "peers_handleBulkDisable",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of peer identifiers to disable",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/bulk-enable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Peer"
|
||||
],
|
||||
"summary": "Bulk enable selected peers.",
|
||||
"operationId": "peers_handleBulkEnable",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of peer identifiers to enable",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/config-mail": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@@ -1444,206 +1324,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/bulk-delete": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Bulk delete selected users.",
|
||||
"operationId": "users_handleBulkDelete",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of user identifiers to delete",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if deletion was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/bulk-disable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Bulk disable selected users.",
|
||||
"operationId": "users_handleBulkDisable",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of user identifiers to disable",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/bulk-enable": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Bulk enable selected users.",
|
||||
"operationId": "users_handleBulkEnable",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of user identifiers to enable",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/bulk-lock": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Bulk lock selected users.",
|
||||
"operationId": "users_handleBulkLock",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of user identifiers to lock",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/bulk-unlock": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Bulk unlock selected users.",
|
||||
"operationId": "users_handleBulkUnlock",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A list of user identifiers to unlock",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No content if action was successful"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/new": {
|
||||
"post": {
|
||||
"produces": [
|
||||
@@ -1870,38 +1550,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/change-password": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Change the password for the given user.",
|
||||
"operationId": "users_handleChangePasswordPost",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/model.Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/{id}/interfaces": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -2057,23 +1705,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.BulkPeerRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"Identifiers"
|
||||
],
|
||||
"properties": {
|
||||
"Identifiers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.ConfigOption-array_string": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2528,10 +2159,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"UserDisplayName": {
|
||||
"description": "the owner display name",
|
||||
"type": "string"
|
||||
},
|
||||
"UserIdentifier": {
|
||||
"description": "the owner",
|
||||
"type": "string"
|
||||
@@ -2676,12 +2303,6 @@
|
||||
"ApiTokenCreated": {
|
||||
"type": "string"
|
||||
},
|
||||
"AuthSources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Department": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2725,11 +2346,14 @@
|
||||
"PeerCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"PersistLocalChanges": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"ProviderName": {
|
||||
"type": "string"
|
||||
},
|
||||
"Source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,17 +16,6 @@ definitions:
|
||||
Timestamp:
|
||||
type: string
|
||||
type: object
|
||||
model.BulkPeerRequest:
|
||||
properties:
|
||||
Identifiers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Reason:
|
||||
type: string
|
||||
required:
|
||||
- Identifiers
|
||||
type: object
|
||||
model.ConfigOption-array_string:
|
||||
properties:
|
||||
Overridable:
|
||||
@@ -333,9 +322,6 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/model.ConfigOption-string'
|
||||
description: the routing table
|
||||
UserDisplayName:
|
||||
description: the owner display name
|
||||
type: string
|
||||
UserIdentifier:
|
||||
description: the owner
|
||||
type: string
|
||||
@@ -431,10 +417,6 @@ definitions:
|
||||
type: string
|
||||
ApiTokenCreated:
|
||||
type: string
|
||||
AuthSources:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
Department:
|
||||
type: string
|
||||
Disabled:
|
||||
@@ -465,10 +447,12 @@ definitions:
|
||||
type: string
|
||||
PeerCount:
|
||||
type: integer
|
||||
PersistLocalChanges:
|
||||
type: boolean
|
||||
Phone:
|
||||
type: string
|
||||
ProviderName:
|
||||
type: string
|
||||
Source:
|
||||
type: string
|
||||
type: object
|
||||
model.WebAuthnCredentialRequest:
|
||||
properties:
|
||||
@@ -1093,84 +1077,6 @@ paths:
|
||||
summary: Update the given peer record.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/bulk-delete:
|
||||
post:
|
||||
operationId: peers_handleBulkDelete
|
||||
parameters:
|
||||
- description: A list of peer identifiers to delete
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if deletion was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk delete selected peers.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/bulk-disable:
|
||||
post:
|
||||
operationId: peers_handleBulkDisable
|
||||
parameters:
|
||||
- description: A list of peer identifiers to disable
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk disable selected peers.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/bulk-enable:
|
||||
post:
|
||||
operationId: peers_handleBulkEnable
|
||||
parameters:
|
||||
- description: A list of peer identifiers to enable
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk enable selected peers.
|
||||
tags:
|
||||
- Peer
|
||||
/peer/config-mail:
|
||||
post:
|
||||
operationId: peers_handleEmailPost
|
||||
@@ -1536,27 +1442,6 @@ paths:
|
||||
summary: Enable the REST API for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/change-password:
|
||||
post:
|
||||
operationId: users_handleChangePasswordPost
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/model.User'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Change the password for the given user.
|
||||
tags:
|
||||
- Users
|
||||
/user/{id}/interfaces:
|
||||
get:
|
||||
operationId: users_handleInterfacesGet
|
||||
@@ -1662,136 +1547,6 @@ paths:
|
||||
summary: Get all user records.
|
||||
tags:
|
||||
- Users
|
||||
/user/bulk-delete:
|
||||
post:
|
||||
operationId: users_handleBulkDelete
|
||||
parameters:
|
||||
- description: A list of user identifiers to delete
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if deletion was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk delete selected users.
|
||||
tags:
|
||||
- Users
|
||||
/user/bulk-disable:
|
||||
post:
|
||||
operationId: users_handleBulkDisable
|
||||
parameters:
|
||||
- description: A list of user identifiers to disable
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk disable selected users.
|
||||
tags:
|
||||
- Users
|
||||
/user/bulk-enable:
|
||||
post:
|
||||
operationId: users_handleBulkEnable
|
||||
parameters:
|
||||
- description: A list of user identifiers to enable
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk enable selected users.
|
||||
tags:
|
||||
- Users
|
||||
/user/bulk-lock:
|
||||
post:
|
||||
operationId: users_handleBulkLock
|
||||
parameters:
|
||||
- description: A list of user identifiers to lock
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk lock selected users.
|
||||
tags:
|
||||
- Users
|
||||
/user/bulk-unlock:
|
||||
post:
|
||||
operationId: users_handleBulkUnlock
|
||||
parameters:
|
||||
- description: A list of user identifiers to unlock
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/model.BulkPeerRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: No content if action was successful
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/model.Error'
|
||||
summary: Bulk unlock selected users.
|
||||
tags:
|
||||
- Users
|
||||
/user/new:
|
||||
post:
|
||||
operationId: users_handleCreatePost
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
"paths": {
|
||||
"/interface/all": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -47,16 +52,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/by-id/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -105,14 +110,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"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, ...).",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -177,14 +182,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -236,16 +241,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/interface/new": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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": [
|
||||
"application/json"
|
||||
@@ -303,16 +308,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"
|
||||
@@ -347,16 +352,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/metrics/by-interface/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -405,16 +410,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/metrics/by-peer/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -463,16 +468,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/metrics/by-user/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -521,16 +526,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/by-id/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own records. Admins can access all records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -580,14 +585,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
],
|
||||
"description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -652,14 +657,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -711,16 +716,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/by-interface/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -760,16 +765,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/by-user/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own records. Admins can access all records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -810,16 +815,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/peer/new": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -877,16 +882,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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"
|
||||
@@ -942,16 +947,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/provisioning/data/peer-config": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own record. Admins can access all records.",
|
||||
"produces": [
|
||||
"text/plain",
|
||||
@@ -1008,16 +1013,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/provisioning/data/peer-qr": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own record. Admins can access all records.",
|
||||
"produces": [
|
||||
"image/png",
|
||||
@@ -1074,16 +1079,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/provisioning/data/user-info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own record. Admins can access all records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1144,16 +1149,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/provisioning/new-peer": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1211,16 +1216,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/all": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -1251,16 +1256,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/by-id/{id}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Normal users can only access their own record. Admins can access all records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1310,14 +1315,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
],
|
||||
"description": "Only admins can update existing records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1382,14 +1387,14 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -1441,16 +1446,16 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"/user/new": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Only admins can create new records.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
@@ -1508,12 +1513,7 @@
|
||||
"$ref": "#/definitions/models.Error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2132,22 +2132,6 @@
|
||||
"minLength": 32,
|
||||
"example": ""
|
||||
},
|
||||
"AuthSources": {
|
||||
"description": "The source of the user. This field is optional.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"db",
|
||||
"ldap",
|
||||
"oauth"
|
||||
]
|
||||
},
|
||||
"readOnly": true,
|
||||
"example": [
|
||||
"db"
|
||||
]
|
||||
},
|
||||
"Department": {
|
||||
"description": "The department of the user. This field is optional.",
|
||||
"type": "string",
|
||||
@@ -2221,6 +2205,20 @@
|
||||
"description": "The phone number of the user. This field is optional.",
|
||||
"type": "string",
|
||||
"example": "+1234546789"
|
||||
},
|
||||
"ProviderName": {
|
||||
"description": "The name of the authentication provider. This field is read-only.",
|
||||
"type": "string",
|
||||
"readOnly": true,
|
||||
"example": ""
|
||||
},
|
||||
"Source": {
|
||||
"description": "The source of the user. This field is optional.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"db"
|
||||
],
|
||||
"example": "db"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -490,18 +490,6 @@ definitions:
|
||||
maxLength: 64
|
||||
minLength: 32
|
||||
type: string
|
||||
AuthSources:
|
||||
description: The source of the user. This field is optional.
|
||||
example:
|
||||
- db
|
||||
items:
|
||||
enum:
|
||||
- db
|
||||
- ldap
|
||||
- oauth
|
||||
type: string
|
||||
readOnly: true
|
||||
type: array
|
||||
Department:
|
||||
description: The department of the user. This field is optional.
|
||||
example: Software Development
|
||||
@@ -564,6 +552,17 @@ definitions:
|
||||
description: The phone number of the user. This field is optional.
|
||||
example: "+1234546789"
|
||||
type: string
|
||||
ProviderName:
|
||||
description: The name of the authentication provider. This field is read-only.
|
||||
example: ""
|
||||
readOnly: true
|
||||
type: string
|
||||
Source:
|
||||
description: The source of the user. This field is optional.
|
||||
enum:
|
||||
- db
|
||||
example: db
|
||||
type: string
|
||||
required:
|
||||
- Identifier
|
||||
type: object
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 7.5 KiB |
@@ -6,9 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>WireGuard Portal API</title>
|
||||
<meta name="description" content="WireGuard Portal API">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{$.BasePath}}/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{$.BasePath}}/app/favicon.ico">
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/app/favicon.ico">
|
||||
</head>
|
||||
|
||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="card-header">SPA Api</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This endpoint is used by the Single Page Application (the Frontend/UI).</p>
|
||||
<a href="{{$.BasePath}}/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="card-header"><b>Version 1</b></div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This is the current main API endpoint.</p>
|
||||
<a href="{{$.BasePath}}/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,17 +43,17 @@
|
||||
<div class="card-header">Version 2</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">This will be a future API version, it is currently work in progress.</p>
|
||||
<a href="{{$.BasePath}}/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
<a href="/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "prt_footer.gohtml" .}}
|
||||
<script src="{{$.BasePath}}/js/jquery.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/jquery.easing.js"></script>
|
||||
<script src="{{$.BasePath}}/js/popper.min.js"></script>
|
||||
<script src="{{$.BasePath}}/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/jquery.min.js"></script>
|
||||
<script src="/js/jquery.easing.js"></script>
|
||||
<script src="/js/popper.min.js"></script>
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><img src="{{$.BasePath}}/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<a class="navbar-brand" href="/"><img src="/img/header-logo.png" alt="Prolicht"/></a>
|
||||
<div id="topNavbar" class="navbar-collapse collapse">
|
||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||
</ul>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
allow-spec-file-load="false"
|
||||
allow-spec-file-download="true"
|
||||
>
|
||||
<img slot="logo" src="{{$.BasePath}}/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
<img slot="logo" src="/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||
|
||||
<p slot="footer" style="margin:0; padding:16px 36px; background-color:#f76b39; color:#fff; text-align:center;" >
|
||||
Copyright © WireGuard Portal {{$.Year}}, version {{$.Version}}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -40,12 +38,6 @@ func (w *writerWrapper) Write(data []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Hijack wraps the Hijack method of the ResponseWriter and returns the hijacked connection.
|
||||
// This is required for websockets to work.
|
||||
func (w *writerWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return http.NewResponseController(w.ResponseWriter).Hijack()
|
||||
}
|
||||
|
||||
// newWriterWrapper returns a new writerWrapper that wraps the given http.ResponseWriter.
|
||||
// It initializes the StatusCode to http.StatusOK.
|
||||
func newWriterWrapper(w http.ResponseWriter) *writerWrapper {
|
||||
|
||||
@@ -4,14 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
@@ -39,7 +35,6 @@ type ApiEndpointSetupFunc func() (ApiVersion, GroupSetupFn)
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
server *routegroup.Bundle
|
||||
root *routegroup.Bundle // root is the web-root (potentially with path prefix)
|
||||
tpl *respond.TemplateRenderer
|
||||
versions map[ApiVersion]*routegroup.Bundle
|
||||
}
|
||||
@@ -80,42 +75,13 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
||||
template.Must(template.New("").ParseFS(apiTemplates, "assets/tpl/*.gohtml")),
|
||||
)
|
||||
|
||||
// Mount base path if configured
|
||||
s.root = s.server
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
s.root = s.server.Mount(s.cfg.Web.BasePath)
|
||||
}
|
||||
|
||||
// Serve static files (under base path if configured)
|
||||
// Serve static files
|
||||
imgFs := http.FS(fsMust(fs.Sub(apiStatics, "assets/img")))
|
||||
s.root.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.root.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.root.HandleFiles("/img", imgFs)
|
||||
s.root.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
if cfg.Web.BasePath == "" {
|
||||
s.root.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
} else {
|
||||
customV0File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v0_swagger.yaml")
|
||||
customV1File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v1_swagger.yaml")
|
||||
customV0File = []byte(strings.Replace(string(customV0File),
|
||||
"basePath: /api/v0", "basePath: "+cfg.Web.BasePath+"/api/v0", 1))
|
||||
customV1File = []byte(strings.Replace(string(customV1File),
|
||||
"basePath: /api/v1", "basePath: "+cfg.Web.BasePath+"/api/v1", 1))
|
||||
|
||||
s.root.HandleFunc("GET /doc/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v0_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV0File)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v1_swagger.yaml" {
|
||||
respond.Data(w, http.StatusOK, "application/yaml", customV1File)
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
s.server.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||
s.server.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||
s.server.HandleFiles("/img", imgFs)
|
||||
s.server.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||
s.server.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||
|
||||
// Setup routes
|
||||
s.setupRoutes(endpoints...)
|
||||
@@ -160,14 +126,14 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
||||
}
|
||||
|
||||
func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
s.root.HandleFunc("GET /api", s.landingPage)
|
||||
s.server.HandleFunc("GET /api", s.landingPage)
|
||||
s.versions = make(map[ApiVersion]*routegroup.Bundle)
|
||||
|
||||
for _, setupFunc := range endpoints {
|
||||
version, groupSetupFn := setupFunc()
|
||||
|
||||
if _, ok := s.versions[version]; !ok {
|
||||
s.versions[version] = s.root.Mount(fmt.Sprintf("/api/%s", version))
|
||||
s.versions[version] = s.server.Mount(fmt.Sprintf("/api/%s", version))
|
||||
|
||||
// OpenAPI documentation (via RapiDoc)
|
||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||
@@ -181,193 +147,38 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||
|
||||
func (s *Server) setupFrontendRoutes() {
|
||||
// Serve static files
|
||||
s.root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app")
|
||||
s.server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app")
|
||||
})
|
||||
|
||||
s.root.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app/favicon.ico")
|
||||
s.server.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app/favicon.ico")
|
||||
})
|
||||
|
||||
// If a custom frontend path is configured, serve files from there when it contains content.
|
||||
// If the directory is empty or missing, populate it with the embedded frontend-dist content first.
|
||||
useEmbeddedFrontend := true
|
||||
if s.cfg.Web.FrontendFilePath != "" {
|
||||
if err := os.MkdirAll(s.cfg.Web.FrontendFilePath, 0755); err != nil {
|
||||
slog.Error("failed to create frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||
} else {
|
||||
ok := true
|
||||
hasFiles, err := dirHasFiles(s.cfg.Web.FrontendFilePath)
|
||||
if err != nil {
|
||||
slog.Error("failed to check frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||
ok = false
|
||||
}
|
||||
if !hasFiles && ok {
|
||||
embeddedFS := fsMust(fs.Sub(frontendStatics, "frontend-dist"))
|
||||
if err := copyEmbedDirToDisk(embeddedFS, s.cfg.Web.FrontendFilePath); err != nil {
|
||||
slog.Error("failed to populate frontend base directory from embedded assets",
|
||||
"path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
// serve files from FS
|
||||
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
||||
useEmbeddedFrontend = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileServer http.Handler
|
||||
if useEmbeddedFrontend {
|
||||
fileServer = http.FileServer(http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
} else {
|
||||
fileServer = http.FileServer(http.Dir(s.cfg.Web.FrontendFilePath))
|
||||
}
|
||||
fileServer = http.StripPrefix(s.cfg.Web.BasePath+"/app", fileServer)
|
||||
|
||||
// Modify index.html and CSS to include the correct base path.
|
||||
var customIndexFile, customCssFile []byte
|
||||
var customCssFileName string
|
||||
if s.cfg.Web.BasePath != "" {
|
||||
customIndexFile, customCssFile, customCssFileName = s.updateBasePathInFrontend(useEmbeddedFrontend)
|
||||
}
|
||||
|
||||
s.root.HandleFunc("GET /app/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// serve a custom index.html file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/" {
|
||||
respond.Data(w, http.StatusOK, "text/html", customIndexFile)
|
||||
return
|
||||
}
|
||||
|
||||
// serve a custom CSS file with the correct base path applied
|
||||
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/assets/"+customCssFileName {
|
||||
respond.Data(w, http.StatusOK, "text/css", customCssFile)
|
||||
return
|
||||
}
|
||||
|
||||
// pass all other requests to the file server
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||
}
|
||||
|
||||
func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "index.gohtml", respond.TplData{
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) rapiDocHandler(version ApiVersion) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
s.tpl.HTML(w, http.StatusOK, "rapidoc.gohtml", respond.TplData{
|
||||
"RapiDocSource": s.cfg.Web.BasePath + "/js/rapidoc-min.js",
|
||||
"BasePath": s.cfg.Web.BasePath,
|
||||
"ApiSpecUrl": fmt.Sprintf("%s/doc/%s_swagger.yaml", s.cfg.Web.BasePath, version),
|
||||
"RapiDocSource": "/js/rapidoc-min.js",
|
||||
"ApiSpecUrl": fmt.Sprintf("/doc/%s_swagger.yaml", version),
|
||||
"Version": internal.Version,
|
||||
"Year": time.Now().Year(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []byte, string) {
|
||||
if s.cfg.Web.BasePath == "" {
|
||||
return nil, nil, "" // nothing to do
|
||||
}
|
||||
|
||||
var customIndexFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customIndexFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "index.html")
|
||||
} else {
|
||||
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
||||
}
|
||||
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
|
||||
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
|
||||
|
||||
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
||||
match := re.FindStringSubmatch(newIndexStr)
|
||||
cssFileName := match[1]
|
||||
|
||||
var customCssFile []byte
|
||||
if useEmbeddedFrontend {
|
||||
customCssFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "assets/"+cssFileName)
|
||||
} else {
|
||||
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
||||
}
|
||||
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||
|
||||
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||
}
|
||||
|
||||
func fsMust(f fs.FS, err error) fs.FS {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// dirHasFiles returns true if the directory contains at least one file (non-directory).
|
||||
func dirHasFiles(dir string) (bool, error) {
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
// Read a few entries; if any entry exists, consider it having files/dirs.
|
||||
// We want to know if there is at least one file; if only subdirs exist, still treat as content.
|
||||
entries, err := d.Readdir(-1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
// check recursively
|
||||
has, err := dirHasFiles(filepath.Join(dir, e.Name()))
|
||||
if err == nil && has {
|
||||
return true, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
// regular file
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// copyEmbedDirToDisk copies the contents of srcFS into dstDir on disk.
|
||||
func copyEmbedDirToDisk(srcFS fs.FS, dstDir string) error {
|
||||
return fs.WalkDir(srcFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := filepath.Join(dstDir, path)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0755)
|
||||
}
|
||||
// ensure parent dir exists
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
// open source file
|
||||
f, err := srcFS.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
out, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, f); err != nil {
|
||||
_ = out.Close()
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
@@ -119,30 +118,3 @@ func (p PeerService) SendPeerEmail(
|
||||
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
||||
return p.peers.GetPeerStats(ctx, id)
|
||||
}
|
||||
|
||||
func (p PeerService) BulkDelete(ctx context.Context, ids []domain.PeerIdentifier) error {
|
||||
for _, id := range ids {
|
||||
if err := p.peers.DeletePeer(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete peer %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PeerService) BulkUpdate(ctx context.Context, ids []domain.PeerIdentifier, updateFn func(*domain.Peer)) error {
|
||||
for _, id := range ids {
|
||||
peer, err := p.peers.GetPeer(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get peer %s: %w", id, err)
|
||||
}
|
||||
|
||||
updateFn(peer)
|
||||
|
||||
if _, err := p.peers.UpdatePeer(ctx, peer); err != nil {
|
||||
return fmt.Errorf("failed to update peer %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
@@ -73,50 +70,6 @@ func (u UserService) DeactivateApi(ctx context.Context, id domain.UserIdentifier
|
||||
return u.users.DeactivateApi(ctx, id)
|
||||
}
|
||||
|
||||
func (u UserService) ChangePassword(
|
||||
ctx context.Context,
|
||||
id domain.UserIdentifier,
|
||||
oldPassword, newPassword string,
|
||||
) (*domain.User, error) {
|
||||
oldPassword = strings.TrimSpace(oldPassword)
|
||||
newPassword = strings.TrimSpace(newPassword)
|
||||
|
||||
if newPassword == "" {
|
||||
return nil, fmt.Errorf("new password must not be empty")
|
||||
}
|
||||
|
||||
// ensure that the new password is different from the old one
|
||||
if oldPassword == newPassword {
|
||||
return nil, fmt.Errorf("new password must be different from the old one")
|
||||
}
|
||||
|
||||
user, err := u.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
// ensure that the user uses the database backend; otherwise we can't change the password
|
||||
if !slices.ContainsFunc(user.Authentications, func(authentication domain.UserAuthentication) bool {
|
||||
return authentication.Source == domain.UserSourceDatabase
|
||||
}) {
|
||||
return nil, fmt.Errorf("user has no linked authentication source that does support password changes")
|
||||
}
|
||||
|
||||
// validate old password
|
||||
if user.CheckPassword(oldPassword) != nil {
|
||||
return nil, fmt.Errorf("current password is invalid")
|
||||
}
|
||||
|
||||
user.Password = domain.PrivateString(newPassword)
|
||||
|
||||
// ensure that the new password is strong enough
|
||||
if err := user.HasWeakPassword(u.cfg.Auth.MinPasswordLength); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u.users.UpdateUser(ctx, user)
|
||||
}
|
||||
|
||||
func (u UserService) GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
|
||||
return u.wg.GetUserPeers(ctx, id)
|
||||
}
|
||||
@@ -128,30 +81,3 @@ func (u UserService) GetUserPeerStats(ctx context.Context, id domain.UserIdentif
|
||||
func (u UserService) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
|
||||
return u.wg.GetUserInterfaces(ctx, id)
|
||||
}
|
||||
|
||||
func (u UserService) BulkDelete(ctx context.Context, ids []domain.UserIdentifier) error {
|
||||
for _, id := range ids {
|
||||
if err := u.users.DeleteUser(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete user %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UserService) BulkUpdate(ctx context.Context, ids []domain.UserIdentifier, updateFn func(*domain.User)) error {
|
||||
for _, id := range ids {
|
||||
user, err := u.users.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user %s: %w", id, err)
|
||||
}
|
||||
|
||||
updateFn(user)
|
||||
|
||||
if _, err := u.users.UpdateUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update user %s: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
@@ -448,17 +449,7 @@ func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
|
||||
|
||||
// isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
|
||||
func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
||||
expectedUrl, err := url.Parse(e.cfg.Web.ExternalUrl)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
returnUrlParsed, err := url.Parse(returnUrl)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if returnUrlParsed.Scheme != expectedUrl.Scheme || returnUrlParsed.Host != expectedUrl.Host {
|
||||
if !strings.HasPrefix(returnUrl, e.cfg.Web.ExternalUrl) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,7 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
// @Router /config/frontend.js [get]
|
||||
func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
basePath := e.cfg.Web.BasePath
|
||||
backendUrl := fmt.Sprintf("%s%s/api/v0", e.cfg.Web.ExternalUrl, basePath)
|
||||
backendUrl := fmt.Sprintf("%s/api/v0", e.cfg.Web.ExternalUrl)
|
||||
if request.Header(r, "x-wg-dev") != "" {
|
||||
referer := request.Header(r, "Referer")
|
||||
host := "localhost"
|
||||
@@ -77,13 +76,12 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||
if err == nil {
|
||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||
}
|
||||
backendUrl = fmt.Sprintf("http://%s:%s%s/api/v0", host,
|
||||
port, basePath) // override if request comes from frontend started with npm run dev
|
||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
||||
port) // override if request comes from frontend started with npm run dev
|
||||
}
|
||||
|
||||
e.tpl.Render(w, http.StatusOK, "frontend_config.js.gotpl", "text/javascript", map[string]any{
|
||||
"BackendUrl": backendUrl,
|
||||
"BasePath": basePath,
|
||||
"Version": internal.Version,
|
||||
"SiteTitle": e.cfg.Web.SiteTitle,
|
||||
"SiteCompanyName": e.cfg.Web.SiteCompanyName,
|
||||
@@ -145,7 +143,6 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
||||
AvailableBackends: controllerFn(),
|
||||
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||
CreateDefaultPeer: e.cfg.Core.CreateDefaultPeer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
|
||||
@@ -42,10 +41,6 @@ type PeerService interface {
|
||||
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
|
||||
// GetPeerStats returns the peer stats for the given interface.
|
||||
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
||||
// BulkDelete deletes multiple peers.
|
||||
BulkDelete(context.Context, []domain.PeerIdentifier) error
|
||||
// BulkUpdate modifies multiple peers.
|
||||
BulkUpdate(context.Context, []domain.PeerIdentifier, func(*domain.Peer)) error
|
||||
}
|
||||
|
||||
type PeerEndpoint struct {
|
||||
@@ -89,9 +84,6 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup.HandleFunc("GET /{id}", e.handleSingleGet())
|
||||
apiGroup.HandleFunc("PUT /{id}", e.handleUpdatePut())
|
||||
apiGroup.HandleFunc("DELETE /{id}", e.handleDelete())
|
||||
apiGroup.HandleFunc("POST /bulk-delete", e.handleBulkDelete())
|
||||
apiGroup.HandleFunc("POST /bulk-enable", e.handleBulkEnable())
|
||||
apiGroup.HandleFunc("POST /bulk-disable", e.handleBulkDisable())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
@@ -529,114 +521,3 @@ func (e PeerEndpoint) getConfigStyle(r *http.Request) string {
|
||||
}
|
||||
return configStyle
|
||||
}
|
||||
|
||||
// handleBulkDelete returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleBulkDelete
|
||||
// @Tags Peer
|
||||
// @Summary Bulk delete selected peers.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to delete"
|
||||
// @Success 204 "No content if deletion was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /peer/bulk-delete [post]
|
||||
func (e PeerEndpoint) handleBulkDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkPeerRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.PeerIdentifier(id)
|
||||
}
|
||||
|
||||
err := e.peerService.BulkDelete(r.Context(), ids)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkEnable returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleBulkEnable
|
||||
// @Tags Peer
|
||||
// @Summary Bulk enable selected peers.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to enable"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /peer/bulk-enable [post]
|
||||
func (e PeerEndpoint) handleBulkEnable() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkPeerRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.PeerIdentifier(id)
|
||||
}
|
||||
|
||||
err := e.peerService.BulkUpdate(r.Context(), ids, func(p *domain.Peer) {
|
||||
p.Disabled = nil
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkDisable returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleBulkDisable
|
||||
// @Tags Peer
|
||||
// @Summary Bulk disable selected peers.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to disable"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /peer/bulk-disable [post]
|
||||
func (e PeerEndpoint) handleBulkDisable() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkPeerRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.PeerIdentifier(id)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := e.peerService.BulkUpdate(r.Context(), ids, func(p *domain.Peer) {
|
||||
p.Disabled = &now
|
||||
p.DisabledReason = domain.DisabledReasonAdmin
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
|
||||
@@ -29,18 +28,12 @@ type UserService interface {
|
||||
ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
// DeactivateApi disables the API for the user with the given id.
|
||||
DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
// ChangePassword changes the password for the user with the given id.
|
||||
ChangePassword(ctx context.Context, id domain.UserIdentifier, oldPassword, newPassword string) (*domain.User, error)
|
||||
// GetUserPeers returns all peers for the given user.
|
||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||
// GetUserPeerStats returns all peer stats for the given user.
|
||||
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
||||
// GetUserInterfaces returns all interfaces for the given user.
|
||||
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
|
||||
// BulkDelete deletes multiple users.
|
||||
BulkDelete(ctx context.Context, ids []domain.UserIdentifier) error
|
||||
// BulkUpdate modifies multiple users.
|
||||
BulkUpdate(ctx context.Context, ids []domain.UserIdentifier, updateFn func(*domain.User)) error
|
||||
}
|
||||
|
||||
type UserEndpoint struct {
|
||||
@@ -82,13 +75,6 @@ func (e UserEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("GET /{id}/interfaces", e.handleInterfacesGet())
|
||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/enable", e.handleApiEnablePost())
|
||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/disable", e.handleApiDisablePost())
|
||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/change-password",
|
||||
e.handleChangePasswordPost())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-delete", e.handleBulkDelete())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-enable", e.handleBulkEnable())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-disable", e.handleBulkDisable())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-lock", e.handleBulkLock())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-unlock", e.handleBulkUnlock())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
@@ -405,255 +391,3 @@ func (e UserEndpoint) handleApiDisablePost() http.HandlerFunc {
|
||||
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
||||
}
|
||||
}
|
||||
|
||||
// handleChangePasswordPost returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleChangePasswordPost
|
||||
// @Tags Users
|
||||
// @Summary Change the password for the given user.
|
||||
// @Produce json
|
||||
// @Success 200 {object} model.User
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/{id}/change-password [post]
|
||||
func (e UserEndpoint) handleChangePasswordPost() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userId := Base64UrlDecode(request.Path(r, "id"))
|
||||
if userId == "" {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
|
||||
return
|
||||
}
|
||||
|
||||
var passwordData struct {
|
||||
OldPassword string `json:"OldPassword"`
|
||||
Password string `json:"Password"`
|
||||
PasswordRepeat string `json:"PasswordRepeat"`
|
||||
}
|
||||
if err := request.BodyJson(r, &passwordData); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if passwordData.OldPassword == "" {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "old password missing"})
|
||||
return
|
||||
}
|
||||
|
||||
if passwordData.Password == "" {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "new password missing"})
|
||||
return
|
||||
}
|
||||
|
||||
if passwordData.OldPassword == passwordData.Password {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "password did not change"})
|
||||
return
|
||||
}
|
||||
|
||||
if passwordData.Password != passwordData.PasswordRepeat {
|
||||
respond.JSON(w, http.StatusBadRequest,
|
||||
model.Error{Code: http.StatusBadRequest, Message: "password mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := e.userService.ChangePassword(r.Context(), domain.UserIdentifier(userId),
|
||||
passwordData.OldPassword, passwordData.Password)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkDelete returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleBulkDelete
|
||||
// @Tags Users
|
||||
// @Summary Bulk delete selected users.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of user identifiers to delete"
|
||||
// @Success 204 "No content if deletion was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/bulk-delete [post]
|
||||
func (e UserEndpoint) handleBulkDelete() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkUserRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.UserIdentifier(id)
|
||||
}
|
||||
|
||||
err := e.userService.BulkDelete(r.Context(), ids)
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkEnable returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleBulkEnable
|
||||
// @Tags Users
|
||||
// @Summary Bulk enable selected users.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of user identifiers to enable"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/bulk-enable [post]
|
||||
func (e UserEndpoint) handleBulkEnable() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkUserRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.UserIdentifier(id)
|
||||
}
|
||||
|
||||
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||
user.Disabled = nil
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkDisable returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleBulkDisable
|
||||
// @Tags Users
|
||||
// @Summary Bulk disable selected users.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of user identifiers to disable"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/bulk-disable [post]
|
||||
func (e UserEndpoint) handleBulkDisable() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkUserRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.UserIdentifier(id)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||
user.Disabled = &now
|
||||
user.DisabledReason = domain.DisabledReasonAdmin
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkLock returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleBulkLock
|
||||
// @Tags Users
|
||||
// @Summary Bulk lock selected users.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of user identifiers to lock"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/bulk-lock [post]
|
||||
func (e UserEndpoint) handleBulkLock() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkUserRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.UserIdentifier(id)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||
user.Locked = &now
|
||||
user.LockedReason = domain.LockedReasonAdmin
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBulkUnlock returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleBulkUnlock
|
||||
// @Tags Users
|
||||
// @Summary Bulk unlock selected users.
|
||||
// @Produce json
|
||||
// @Param request body model.BulkPeerRequest true "A list of user identifiers to unlock"
|
||||
// @Success 204 "No content if action was successful"
|
||||
// @Failure 400 {object} model.Error
|
||||
// @Failure 500 {object} model.Error
|
||||
// @Router /user/bulk-unlock [post]
|
||||
func (e UserEndpoint) handleBulkUnlock() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.BulkUserRequest
|
||||
if err := request.BodyJson(r, &req); err != nil {
|
||||
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||
for i, id := range req.Identifiers {
|
||||
ids[i] = domain.UserIdentifier(id)
|
||||
}
|
||||
|
||||
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||
user.Locked = nil
|
||||
})
|
||||
if err != nil {
|
||||
respond.JSON(w, http.StatusInternalServerError,
|
||||
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
respond.Status(w, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-pkgz/routegroup"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/app"
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type WebsocketEventBus interface {
|
||||
Subscribe(topic string, fn any) error
|
||||
Unsubscribe(topic string, fn any) error
|
||||
}
|
||||
|
||||
type WebsocketEndpoint struct {
|
||||
authenticator Authenticator
|
||||
bus WebsocketEventBus
|
||||
|
||||
upgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
|
||||
return &WebsocketEndpoint{
|
||||
authenticator: auth,
|
||||
bus: bus,
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
return strings.HasPrefix(origin, cfg.Web.ExternalUrl)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e WebsocketEndpoint) GetName() string {
|
||||
return "WebsocketEndpoint"
|
||||
}
|
||||
|
||||
func (e WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket())
|
||||
}
|
||||
|
||||
// wsMessage represents a message sent over websocket to the frontend
|
||||
type wsMessage struct {
|
||||
Type string `json:"type"` // either "peer_stats" or "interface_stats"
|
||||
Data any `json:"data"` // domain.TrafficDelta
|
||||
}
|
||||
|
||||
func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := e.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
defer cancel()
|
||||
|
||||
writeMutex := sync.Mutex{}
|
||||
writeJSON := func(msg wsMessage) error {
|
||||
writeMutex.Lock()
|
||||
defer writeMutex.Unlock()
|
||||
return conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
peerStatsHandler := func(status domain.TrafficDelta) {
|
||||
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
|
||||
}
|
||||
interfaceStatsHandler := func(status domain.TrafficDelta) {
|
||||
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
|
||||
}
|
||||
|
||||
_ = e.bus.Subscribe(app.TopicPeerStatsUpdated, peerStatsHandler)
|
||||
defer e.bus.Unsubscribe(app.TopicPeerStatsUpdated, peerStatsHandler)
|
||||
_ = e.bus.Subscribe(app.TopicInterfaceStatsUpdated, interfaceStatsHandler)
|
||||
defer e.bus.Unsubscribe(app.TopicInterfaceStatsUpdated, interfaceStatsHandler)
|
||||
|
||||
// Keep connection open until client disconnects or context is cancelled
|
||||
go func() {
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
WGPORTAL_BACKEND_BASE_URL="{{ $.BackendUrl }}";
|
||||
WGPORTAL_BASE_PATH="{{ $.BasePath }}";
|
||||
WGPORTAL_VERSION="{{ $.Version }}";
|
||||
WGPORTAL_SITE_TITLE="{{ $.SiteTitle }}";
|
||||
WGPORTAL_SITE_COMPANY_NAME="{{ $.SiteCompanyName }}";
|
||||
|
||||
@@ -49,11 +49,7 @@ func NewSessionWrapper(cfg *config.Config) *SessionWrapper {
|
||||
sessionManager.Cookie.Secure = strings.HasPrefix(cfg.Web.ExternalUrl, "https")
|
||||
sessionManager.Cookie.HttpOnly = true
|
||||
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
|
||||
if cfg.Web.BasePath != "" {
|
||||
sessionManager.Cookie.Path = cfg.Web.BasePath
|
||||
} else {
|
||||
sessionManager.Cookie.Path = "/"
|
||||
}
|
||||
sessionManager.Cookie.Path = "/"
|
||||
sessionManager.Cookie.Persist = false
|
||||
|
||||
wrappedSessionManager := &SessionWrapper{sessionManager}
|
||||
|
||||
@@ -14,7 +14,6 @@ type Settings struct {
|
||||
MinPasswordLength int `json:"MinPasswordLength"`
|
||||
AvailableBackends []SettingsBackendNames `json:"AvailableBackends"`
|
||||
LoginFormVisible bool `json:"LoginFormVisible"`
|
||||
CreateDefaultPeer bool `json:"CreateDefaultPeer"`
|
||||
}
|
||||
|
||||
type SettingsBackendNames struct {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package model
|
||||
|
||||
type BulkPeerRequest struct {
|
||||
Identifiers []string `json:"Identifiers" binding:"required"`
|
||||
Reason string `json:"Reason"`
|
||||
}
|
||||
|
||||
type BulkUserRequest struct {
|
||||
Identifiers []string `json:"Identifiers" binding:"required"`
|
||||
}
|
||||
@@ -9,16 +9,15 @@ import (
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
||||
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
||||
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
|
||||
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
||||
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
||||
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
|
||||
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
||||
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
||||
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
|
||||
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
||||
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
||||
|
||||
ListenPort int `json:"ListenPort"` // the listening port, for example: 51820
|
||||
Addresses []string `json:"Addresses"` // the interface ip addresses
|
||||
@@ -66,7 +65,6 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
SaveConfig: src.SaveConfig,
|
||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||
ListenPort: src.ListenPort,
|
||||
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||
Dns: internal.SliceString(src.DnsStr),
|
||||
@@ -153,7 +151,6 @@ func NewDomainInterface(src *Interface) *domain.Interface {
|
||||
PreDown: src.PreDown,
|
||||
PostDown: src.PostDown,
|
||||
SaveConfig: src.SaveConfig,
|
||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||
DisplayName: src.DisplayName,
|
||||
Type: domain.InterfaceType(src.Mode),
|
||||
Backend: domain.InterfaceBackend(src.Backend),
|
||||
|
||||
@@ -3,15 +3,15 @@ package model
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Identifier string `json:"Identifier"`
|
||||
Email string `json:"Email"`
|
||||
AuthSources []string `json:"AuthSources"`
|
||||
IsAdmin bool `json:"IsAdmin"`
|
||||
Identifier string `json:"Identifier"`
|
||||
Email string `json:"Email"`
|
||||
Source string `json:"Source"`
|
||||
ProviderName string `json:"ProviderName"`
|
||||
IsAdmin bool `json:"IsAdmin"`
|
||||
|
||||
Firstname string `json:"Firstname"`
|
||||
Lastname string `json:"Lastname"`
|
||||
@@ -29,8 +29,6 @@ type User struct {
|
||||
ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
|
||||
ApiEnabled bool `json:"ApiEnabled"`
|
||||
|
||||
PersistLocalChanges bool `json:"PersistLocalChanges"`
|
||||
|
||||
// Calculated
|
||||
|
||||
PeerCount int `json:"PeerCount"`
|
||||
@@ -38,26 +36,24 @@ type User struct {
|
||||
|
||||
func NewUser(src *domain.User, exposeCreds bool) *User {
|
||||
u := &User{
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
AuthSources: internal.Map(src.Authentications, func(authentication domain.UserAuthentication) string {
|
||||
return string(authentication.Source)
|
||||
}),
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: "", // never fill password
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: src.IsLocked(),
|
||||
LockedReason: src.LockedReason,
|
||||
ApiToken: "", // by default, do not expose API token
|
||||
ApiTokenCreated: src.ApiTokenCreated,
|
||||
ApiEnabled: src.IsApiEnabled(),
|
||||
PersistLocalChanges: src.PersistLocalChanges,
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: string(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: "", // never fill password
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: src.IsLocked(),
|
||||
LockedReason: src.LockedReason,
|
||||
ApiToken: "", // by default, do not expose API token
|
||||
ApiTokenCreated: src.ApiTokenCreated,
|
||||
ApiEnabled: src.IsApiEnabled(),
|
||||
|
||||
PeerCount: src.LinkedPeerCount,
|
||||
}
|
||||
@@ -81,21 +77,22 @@ func NewUsers(src []domain.User) []User {
|
||||
func NewDomainUser(src *User) *domain.User {
|
||||
now := time.Now()
|
||||
res := &domain.User{
|
||||
Identifier: domain.UserIdentifier(src.Identifier),
|
||||
Email: src.Email,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: domain.PrivateString(src.Password),
|
||||
Disabled: nil, // set below
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: nil, // set below
|
||||
LockedReason: src.LockedReason,
|
||||
LinkedPeerCount: src.PeerCount,
|
||||
PersistLocalChanges: src.PersistLocalChanges,
|
||||
Identifier: domain.UserIdentifier(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: domain.UserSource(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
Phone: src.Phone,
|
||||
Department: src.Department,
|
||||
Notes: src.Notes,
|
||||
Password: domain.PrivateString(src.Password),
|
||||
Disabled: nil, // set below
|
||||
DisabledReason: src.DisabledReason,
|
||||
Locked: nil, // set below
|
||||
LockedReason: src.LockedReason,
|
||||
LinkedPeerCount: src.PeerCount,
|
||||
}
|
||||
|
||||
if src.Disabled {
|
||||
|
||||
@@ -48,12 +48,12 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup.Use(e.authenticator.LoggedIn(ScopeAdmin))
|
||||
|
||||
apiGroup.HandleFunc("GET /all", e.handleAllGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id...}", e.handleByIdGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
||||
|
||||
apiGroup.HandleFunc("GET /prepare", e.handlePrepareGet())
|
||||
apiGroup.HandleFunc("POST /new", e.handleCreatePost())
|
||||
apiGroup.HandleFunc("PUT /by-id/{id...}", e.handleUpdatePut())
|
||||
apiGroup.HandleFunc("DELETE /by-id/{id...}", e.handleDelete())
|
||||
apiGroup.HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
||||
apiGroup.HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
|
||||
@@ -44,10 +44,10 @@ func (e MetricsEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup := g.Mount("/metrics")
|
||||
apiGroup.Use(e.authenticator.LoggedIn())
|
||||
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /by-interface/{id...}",
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /by-interface/{id}",
|
||||
e.handleMetricsForInterfaceGet())
|
||||
apiGroup.HandleFunc("GET /by-user/{id...}", e.handleMetricsForUserGet())
|
||||
apiGroup.HandleFunc("GET /by-peer/{id...}", e.handleMetricsForPeerGet())
|
||||
apiGroup.HandleFunc("GET /by-user/{id}", e.handleMetricsForUserGet())
|
||||
apiGroup.HandleFunc("GET /by-peer/{id}", e.handleMetricsForPeerGet())
|
||||
}
|
||||
|
||||
// handleMetricsForInterfaceGet returns a gorm Handler function.
|
||||
|
||||
@@ -47,15 +47,15 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup := g.Mount("/peer")
|
||||
apiGroup.Use(e.authenticator.LoggedIn())
|
||||
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /by-interface/{id...}",
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /by-interface/{id}",
|
||||
e.handleAllForInterfaceGet())
|
||||
apiGroup.HandleFunc("GET /by-user/{id...}", e.handleAllForUserGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id...}", e.handleByIdGet())
|
||||
apiGroup.HandleFunc("GET /by-user/{id}", e.handleAllForUserGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
||||
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /prepare/{id...}", e.handlePrepareGet())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /prepare/{id}", e.handlePrepareGet())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id...}", e.handleUpdatePut())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id...}", e.handleDelete())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||
|
||||
@@ -47,10 +47,10 @@ func (e UserEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
||||
apiGroup.Use(e.authenticator.LoggedIn())
|
||||
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /all", e.handleAllGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id...}", e.handleByIdGet())
|
||||
apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id...}", e.handleUpdatePut())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id...}", e.handleDelete())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut())
|
||||
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
|
||||
@@ -3,7 +3,6 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/h44z/wg-portal/internal"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
@@ -14,7 +13,9 @@ type User struct {
|
||||
// The email address of the user. This field is optional.
|
||||
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
|
||||
// The source of the user. This field is optional.
|
||||
AuthSources []string `json:"AuthSources" readonly:"true" binding:"oneof=db ldap oauth" example:"db"`
|
||||
Source string `json:"Source" binding:"oneof=db" example:"db"`
|
||||
// The name of the authentication provider. This field is read-only.
|
||||
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
|
||||
// If this field is set, the user is an admin.
|
||||
IsAdmin bool `json:"IsAdmin" example:"false"`
|
||||
|
||||
@@ -51,11 +52,10 @@ type User struct {
|
||||
|
||||
func NewUser(src *domain.User, exposeCredentials bool) *User {
|
||||
u := &User{
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
AuthSources: internal.Map(src.Authentications, func(authentication domain.UserAuthentication) string {
|
||||
return string(authentication.Source)
|
||||
}),
|
||||
Identifier: string(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: string(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
@@ -93,6 +93,8 @@ func NewDomainUser(src *User) *domain.User {
|
||||
res := &domain.User{
|
||||
Identifier: domain.UserIdentifier(src.Identifier),
|
||||
Email: src.Email,
|
||||
Source: domain.UserSource(src.Source),
|
||||
ProviderName: src.ProviderName,
|
||||
IsAdmin: src.IsAdmin,
|
||||
Firstname: src.Firstname,
|
||||
Lastname: src.Lastname,
|
||||
|
||||
@@ -129,6 +129,8 @@ func (a *App) createDefaultUser(ctx context.Context) error {
|
||||
},
|
||||
Identifier: adminUserId,
|
||||
Email: "admin@wgportal.local",
|
||||
Source: domain.UserSourceDatabase,
|
||||
ProviderName: "",
|
||||
IsAdmin: true,
|
||||
Firstname: "WireGuard Portal",
|
||||
Lastname: "Admin",
|
||||
|
||||
@@ -29,8 +29,8 @@ type UserManager interface {
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
// RegisterUser creates a new user in the database.
|
||||
RegisterUser(ctx context.Context, user *domain.User) error
|
||||
// UpdateUserInternal updates an existing user in the database.
|
||||
UpdateUserInternal(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
// UpdateUser updates an existing user in the database.
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type EventBus interface {
|
||||
@@ -99,7 +99,7 @@ type Authenticator struct {
|
||||
}
|
||||
|
||||
// NewAuthenticator creates a new Authenticator instance.
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, users UserManager) (
|
||||
func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserManager) (
|
||||
*Authenticator,
|
||||
error,
|
||||
) {
|
||||
@@ -107,7 +107,7 @@ func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, u
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
users: users,
|
||||
callbackUrlPrefix: fmt.Sprintf("%s%s/api/v0", extUrl, basePath),
|
||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
||||
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
||||
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func (a *Authenticator) setupExternalAuthProviders(
|
||||
}
|
||||
for i := range ldap { // LDAP
|
||||
providerCfg := &ldap[i]
|
||||
providerId := strings.ToLower(providerCfg.ProviderName)
|
||||
providerId := strings.ToLower(providerCfg.URL)
|
||||
|
||||
if _, exists := a.ldapAuthenticators[providerId]; exists {
|
||||
// this is an unrecoverable error, we cannot register the same provider twice
|
||||
@@ -354,45 +354,21 @@ func (a *Authenticator) passwordAuthentication(
|
||||
var ldapProvider AuthenticatorLdap
|
||||
|
||||
var userInDatabase = false
|
||||
var userSource domain.UserSource
|
||||
existingUser, err := a.users.GetUser(ctx, identifier)
|
||||
if err == nil {
|
||||
userInDatabase = true
|
||||
userSource = existingUser.Source
|
||||
}
|
||||
if userInDatabase && (existingUser.IsLocked() || existingUser.IsDisabled()) {
|
||||
return nil, errors.New("user is locked")
|
||||
}
|
||||
|
||||
authOK := false
|
||||
if userInDatabase {
|
||||
// User is already in db, search for authentication sources which support password authentication and
|
||||
// validate the password.
|
||||
for _, authentication := range existingUser.Authentications {
|
||||
if authentication.Source == domain.UserSourceDatabase {
|
||||
err := existingUser.CheckPassword(password)
|
||||
if err == nil {
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if authentication.Source == domain.UserSourceLdap {
|
||||
ldapProvider, ok := a.ldapAuthenticators[strings.ToLower(authentication.ProviderName)]
|
||||
if !ok {
|
||||
continue // ldap provider not found, skip further checks
|
||||
}
|
||||
err := ldapProvider.PlaintextAuthentication(identifier, password)
|
||||
if err == nil {
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User is not yet in the db, check ldap providers which have registration enabled.
|
||||
// If the user is found, check the password - on success, sync it to the db.
|
||||
if !userInDatabase || userSource == domain.UserSourceLdap {
|
||||
// search user in ldap if registration is enabled
|
||||
for _, ldapAuth := range a.ldapAuthenticators {
|
||||
if !ldapAuth.RegistrationEnabled() {
|
||||
continue // ldap provider does not support registration, skip further checks
|
||||
if !userInDatabase && !ldapAuth.RegistrationEnabled() {
|
||||
continue
|
||||
}
|
||||
|
||||
rawUserInfo, err := ldapAuth.GetUserInfo(context.Background(), identifier)
|
||||
@@ -403,39 +379,55 @@ func (a *Authenticator) passwordAuthentication(
|
||||
}
|
||||
continue // user not found / other ldap error
|
||||
}
|
||||
|
||||
// user found, check if the password is correct
|
||||
err = ldapAuth.PlaintextAuthentication(identifier, password)
|
||||
if err != nil {
|
||||
continue // password is incorrect, skip further checks
|
||||
}
|
||||
|
||||
// create a new user in the db
|
||||
ldapUserInfo, err = ldapAuth.ParseUserInfo(rawUserInfo)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse ldap user info",
|
||||
"source", ldapAuth.GetName(), "identifier", identifier, "error", err)
|
||||
continue
|
||||
}
|
||||
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(), true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
|
||||
existingUser = user
|
||||
slog.Debug("created new LDAP user in db",
|
||||
"identifier", user.Identifier, "provider", ldapProvider.GetName())
|
||||
// ldap user found
|
||||
userSource = domain.UserSourceLdap
|
||||
ldapProvider = ldapAuth
|
||||
|
||||
authOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !authOK {
|
||||
return nil, errors.New("failed to authenticate user")
|
||||
if userSource == "" {
|
||||
slog.Warn("no user source found for user",
|
||||
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
return existingUser, nil
|
||||
if userSource == domain.UserSourceLdap && ldapProvider == nil {
|
||||
slog.Warn("no ldap provider found for user",
|
||||
"identifier", identifier, "ldapProviderCount", len(a.ldapAuthenticators), "inDb", userInDatabase)
|
||||
return nil, errors.New("ldap provider not found")
|
||||
}
|
||||
|
||||
switch userSource {
|
||||
case domain.UserSourceDatabase:
|
||||
err = existingUser.CheckPassword(password)
|
||||
case domain.UserSourceLdap:
|
||||
err = ldapProvider.PlaintextAuthentication(identifier, password)
|
||||
default:
|
||||
err = errors.New("no authentication backend available")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
|
||||
if !userInDatabase {
|
||||
user, err := a.processUserInfo(ctx, ldapUserInfo, domain.UserSourceLdap, ldapProvider.GetName(),
|
||||
ldapProvider.RegistrationEnabled())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process user information: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
} else {
|
||||
return existingUser, nil
|
||||
}
|
||||
}
|
||||
|
||||
// endregion password authentication
|
||||
@@ -598,34 +590,17 @@ func (a *Authenticator) registerNewUser(
|
||||
source domain.UserSource,
|
||||
provider string,
|
||||
) (*domain.User, error) {
|
||||
ctxUserInfo := domain.GetUserInfo(ctx)
|
||||
now := time.Now()
|
||||
|
||||
// convert user info to domain.User
|
||||
user := &domain.User{
|
||||
Identifier: userInfo.Identifier,
|
||||
Email: userInfo.Email,
|
||||
IsAdmin: false,
|
||||
Firstname: userInfo.Firstname,
|
||||
Lastname: userInfo.Lastname,
|
||||
Phone: userInfo.Phone,
|
||||
Department: userInfo.Department,
|
||||
Authentications: []domain.UserAuthentication{
|
||||
{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: ctxUserInfo.UserId(),
|
||||
UpdatedBy: ctxUserInfo.UserId(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: userInfo.Identifier,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
},
|
||||
},
|
||||
}
|
||||
if userInfo.AdminInfoAvailable && userInfo.IsAdmin {
|
||||
user.IsAdmin = true
|
||||
Identifier: userInfo.Identifier,
|
||||
Email: userInfo.Email,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
IsAdmin: userInfo.IsAdmin,
|
||||
Firstname: userInfo.Firstname,
|
||||
Lastname: userInfo.Lastname,
|
||||
Phone: userInfo.Phone,
|
||||
Department: userInfo.Department,
|
||||
}
|
||||
|
||||
err := a.users.RegisterUser(ctx, user)
|
||||
@@ -635,7 +610,6 @@ func (a *Authenticator) registerNewUser(
|
||||
|
||||
slog.Debug("registered user from external authentication provider",
|
||||
"user", user.Identifier,
|
||||
"adminInfoAvailable", userInfo.AdminInfoAvailable,
|
||||
"isAdmin", user.IsAdmin,
|
||||
"provider", source)
|
||||
|
||||
@@ -669,39 +643,6 @@ func (a *Authenticator) updateExternalUser(
|
||||
return nil // user is locked or disabled, do not update
|
||||
}
|
||||
|
||||
// Update authentication sources
|
||||
foundAuthSource := false
|
||||
for _, auth := range existingUser.Authentications {
|
||||
if auth.Source == source && auth.ProviderName == provider {
|
||||
foundAuthSource = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundAuthSource {
|
||||
ctxUserInfo := domain.GetUserInfo(ctx)
|
||||
now := time.Now()
|
||||
existingUser.Authentications = append(existingUser.Authentications, domain.UserAuthentication{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: ctxUserInfo.UserId(),
|
||||
UpdatedBy: ctxUserInfo.UserId(),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: existingUser.Identifier,
|
||||
Source: source,
|
||||
ProviderName: provider,
|
||||
})
|
||||
}
|
||||
|
||||
if existingUser.PersistLocalChanges {
|
||||
if !foundAuthSource {
|
||||
// Even if local changes are persisted, we need to save the new authentication source
|
||||
_, err := a.users.UpdateUserInternal(ctx, existingUser)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
isChanged := false
|
||||
if existingUser.Email != userInfo.Email {
|
||||
existingUser.Email = userInfo.Email
|
||||
@@ -723,23 +664,32 @@ func (a *Authenticator) updateExternalUser(
|
||||
existingUser.Department = userInfo.Department
|
||||
isChanged = true
|
||||
}
|
||||
if userInfo.AdminInfoAvailable && existingUser.IsAdmin != userInfo.IsAdmin {
|
||||
if existingUser.IsAdmin != userInfo.IsAdmin {
|
||||
existingUser.IsAdmin = userInfo.IsAdmin
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
if isChanged || !foundAuthSource {
|
||||
_, err := a.users.UpdateUserInternal(ctx, existingUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
slog.Debug("updated user with data from external authentication provider",
|
||||
"user", existingUser.Identifier,
|
||||
"adminInfoAvailable", userInfo.AdminInfoAvailable,
|
||||
"isAdmin", existingUser.IsAdmin,
|
||||
"provider", source)
|
||||
if existingUser.Source != source {
|
||||
existingUser.Source = source
|
||||
isChanged = true
|
||||
}
|
||||
if existingUser.ProviderName != provider {
|
||||
existingUser.ProviderName = provider
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
if !isChanged {
|
||||
return nil // nothing to update
|
||||
}
|
||||
|
||||
_, err := a.users.UpdateUser(ctx, existingUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
slog.Debug("updated user with data from external authentication provider",
|
||||
"user", existingUser.Identifier,
|
||||
"isAdmin", existingUser.IsAdmin,
|
||||
"provider", source)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,7 +20,18 @@ type LdapAuthenticator struct {
|
||||
}
|
||||
|
||||
func newLdapAuthenticator(_ context.Context, cfg *config.LdapProvider) (*LdapAuthenticator, error) {
|
||||
return &LdapAuthenticator{cfg: cfg}, nil
|
||||
var provider = &LdapAuthenticator{}
|
||||
|
||||
provider.cfg = cfg
|
||||
|
||||
dn, err := ldap.ParseDN(cfg.AdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse admin group DN: %w", err)
|
||||
}
|
||||
provider.cfg.FieldMap = provider.getLdapFieldMapping(cfg.FieldMap)
|
||||
provider.cfg.ParsedAdminGroupDN = dn
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// GetName returns the name of the LDAP authenticator.
|
||||
@@ -127,27 +138,56 @@ func (l LdapAuthenticator) GetUserInfo(_ context.Context, userId domain.UserIden
|
||||
|
||||
// ParseUserInfo parses the user information from the LDAP server into a domain.AuthenticatorUserInfo struct.
|
||||
func (l LdapAuthenticator) ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error) {
|
||||
isAdmin := false
|
||||
adminInfoAvailable := false
|
||||
if l.cfg.FieldMap.GroupMembership != "" {
|
||||
adminInfoAvailable = true
|
||||
var err error
|
||||
isAdmin, err = internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
}
|
||||
isAdmin, err := internal.LdapIsMemberOf(raw[l.cfg.FieldMap.GroupMembership].([][]byte), l.cfg.ParsedAdminGroupDN)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check admin group: %w", err)
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
AdminInfoAvailable: adminInfoAvailable,
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, l.cfg.FieldMap.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, l.cfg.FieldMap.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, l.cfg.FieldMap.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, l.cfg.FieldMap.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, l.cfg.FieldMap.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, l.cfg.FieldMap.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (l LdapAuthenticator) getLdapFieldMapping(f config.LdapFields) config.LdapFields {
|
||||
defaultMap := config.LdapFields{
|
||||
BaseFields: config.BaseFields{
|
||||
UserIdentifier: "mail",
|
||||
Email: "mail",
|
||||
Firstname: "givenName",
|
||||
Lastname: "sn",
|
||||
Phone: "telephoneNumber",
|
||||
Department: "department",
|
||||
},
|
||||
GroupMembership: "memberOf",
|
||||
}
|
||||
if f.UserIdentifier != "" {
|
||||
defaultMap.UserIdentifier = f.UserIdentifier
|
||||
}
|
||||
if f.Email != "" {
|
||||
defaultMap.Email = f.Email
|
||||
}
|
||||
if f.Firstname != "" {
|
||||
defaultMap.Firstname = f.Firstname
|
||||
}
|
||||
if f.Lastname != "" {
|
||||
defaultMap.Lastname = f.Lastname
|
||||
}
|
||||
if f.Phone != "" {
|
||||
defaultMap.Phone = f.Phone
|
||||
}
|
||||
if f.Department != "" {
|
||||
defaultMap.Department = f.Department
|
||||
}
|
||||
if f.GroupMembership != "" {
|
||||
defaultMap.GroupMembership = f.GroupMembership
|
||||
}
|
||||
|
||||
return defaultMap
|
||||
}
|
||||
|
||||
@@ -19,16 +19,15 @@ import (
|
||||
// PlainOauthAuthenticator is an authenticator that uses OAuth for authentication.
|
||||
// User information is retrieved from the specified user info endpoint.
|
||||
type PlainOauthAuthenticator struct {
|
||||
name string
|
||||
cfg *oauth2.Config
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
sensitiveInfoLogging bool
|
||||
allowedDomains []string
|
||||
name string
|
||||
cfg *oauth2.Config
|
||||
userInfoEndpoint string
|
||||
client *http.Client
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
allowedDomains []string
|
||||
}
|
||||
|
||||
func newPlainOauthAuthenticator(
|
||||
@@ -58,7 +57,6 @@ func newPlainOauthAuthenticator(
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
||||
provider.allowedDomains = cfg.AllowedDomains
|
||||
|
||||
return provider, nil
|
||||
@@ -112,10 +110,6 @@ func (p PlainOauthAuthenticator) GetUserInfo(
|
||||
|
||||
response, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
if p.sensitiveInfoLogging {
|
||||
slog.Debug("OAuth: failed to get user info", "endpoint", p.userInfoEndpoint,
|
||||
"token", token, "error", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
defer internal.LogClose(response.Body)
|
||||
@@ -127,15 +121,11 @@ func (p PlainOauthAuthenticator) GetUserInfo(
|
||||
var userFields map[string]any
|
||||
err = json.Unmarshal(contents, &userFields)
|
||||
if err != nil {
|
||||
if p.sensitiveInfoLogging {
|
||||
slog.Debug("OAuth: failed to parse user info", "endpoint", p.userInfoEndpoint,
|
||||
"token", token, "contents", contents, "error", err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
|
||||
if p.userInfoLogging {
|
||||
slog.Debug("OAuth: user info debug",
|
||||
slog.Debug("OAuth user info",
|
||||
"source", p.name,
|
||||
"info", string(contents))
|
||||
}
|
||||
|
||||
@@ -16,16 +16,15 @@ import (
|
||||
|
||||
// OidcAuthenticator is an authenticator for OpenID Connect providers.
|
||||
type OidcAuthenticator struct {
|
||||
name string
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
sensitiveInfoLogging bool
|
||||
allowedDomains []string
|
||||
name string
|
||||
provider *oidc.Provider
|
||||
verifier *oidc.IDTokenVerifier
|
||||
cfg *oauth2.Config
|
||||
userInfoMapping config.OauthFields
|
||||
userAdminMapping *config.OauthAdminMapping
|
||||
registrationEnabled bool
|
||||
userInfoLogging bool
|
||||
allowedDomains []string
|
||||
}
|
||||
|
||||
func newOidcAuthenticator(
|
||||
@@ -59,7 +58,6 @@ func newOidcAuthenticator(
|
||||
provider.userAdminMapping = &cfg.AdminMapping
|
||||
provider.registrationEnabled = cfg.RegistrationEnabled
|
||||
provider.userInfoLogging = cfg.LogUserInfo
|
||||
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
|
||||
provider.allowedDomains = cfg.AllowedDomains
|
||||
|
||||
return provider, nil
|
||||
@@ -104,40 +102,24 @@ func (o OidcAuthenticator) GetUserInfo(ctx context.Context, token *oauth2.Token,
|
||||
) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
if o.sensitiveInfoLogging {
|
||||
slog.Debug("OIDC: token does not contain id_token", "token", token, "nonce", nonce)
|
||||
}
|
||||
return nil, errors.New("token does not contain id_token")
|
||||
}
|
||||
idToken, err := o.verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
if o.sensitiveInfoLogging {
|
||||
slog.Debug("OIDC: failed to validate id_token", "token", token, "id_token", rawIDToken, "nonce", nonce,
|
||||
"error",
|
||||
err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to validate id_token: %w", err)
|
||||
}
|
||||
if idToken.Nonce != nonce {
|
||||
if o.sensitiveInfoLogging {
|
||||
slog.Debug("OIDC: id_token nonce mismatch", "token", token, "id_token", idToken, "nonce", nonce)
|
||||
}
|
||||
return nil, errors.New("nonce mismatch")
|
||||
}
|
||||
|
||||
var tokenFields map[string]any
|
||||
if err = idToken.Claims(&tokenFields); err != nil {
|
||||
if o.sensitiveInfoLogging {
|
||||
slog.Debug("OIDC: failed to parse extra claims", "token", token, "id_token", idToken, "nonce", nonce,
|
||||
"error",
|
||||
err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse extra claims: %w", err)
|
||||
}
|
||||
|
||||
if o.userInfoLogging {
|
||||
contents, _ := json.Marshal(tokenFields)
|
||||
slog.Debug("OIDC: user info debug",
|
||||
slog.Debug("OIDC user info",
|
||||
"source", o.name,
|
||||
"info", string(contents))
|
||||
}
|
||||
|
||||
@@ -15,11 +15,9 @@ func parseOauthUserInfo(
|
||||
raw map[string]any,
|
||||
) (*domain.AuthenticatorUserInfo, error) {
|
||||
var isAdmin bool
|
||||
var adminInfoAvailable bool
|
||||
|
||||
// first try to match the is_admin field against the given regex
|
||||
if mapping.IsAdmin != "" {
|
||||
adminInfoAvailable = true
|
||||
re := adminMapping.GetAdminValueRegex()
|
||||
if re.MatchString(strings.TrimSpace(internal.MapDefaultString(raw, mapping.IsAdmin, ""))) {
|
||||
isAdmin = true
|
||||
@@ -28,7 +26,6 @@ func parseOauthUserInfo(
|
||||
|
||||
// next try to parse the user's groups
|
||||
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
|
||||
adminInfoAvailable = true
|
||||
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
|
||||
re := adminMapping.GetAdminGroupRegex()
|
||||
for _, group := range userGroups {
|
||||
@@ -40,14 +37,13 @@ func parseOauthUserInfo(
|
||||
}
|
||||
|
||||
userInfo := &domain.AuthenticatorUserInfo{
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
AdminInfoAvailable: adminInfoAvailable,
|
||||
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
|
||||
Email: internal.MapDefaultString(raw, mapping.Email, ""),
|
||||
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
|
||||
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
|
||||
Phone: internal.MapDefaultString(raw, mapping.Phone, ""),
|
||||
Department: internal.MapDefaultString(raw, mapping.Department, ""),
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
return userInfo, nil
|
||||
|
||||
@@ -23,8 +23,8 @@ type WebAuthnUserManager interface {
|
||||
GetUser(context.Context, domain.UserIdentifier) (*domain.User, error)
|
||||
// GetUserByWebAuthnCredential returns a user by its WebAuthn ID.
|
||||
GetUserByWebAuthnCredential(ctx context.Context, credentialIdBase64 string) (*domain.User, error)
|
||||
// UpdateUserInternal updates an existing user in the database.
|
||||
UpdateUserInternal(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
// UpdateUser updates an existing user in the database.
|
||||
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
}
|
||||
|
||||
type WebAuthnAuthenticator struct {
|
||||
@@ -89,7 +89,7 @@ func (a *WebAuthnAuthenticator) StartWebAuthnRegistration(ctx context.Context, u
|
||||
|
||||
if user.WebAuthnId == "" {
|
||||
user.GenerateWebAuthnId()
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to store webauthn id to user: %w", err)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (a *WebAuthnAuthenticator) FinishWebAuthnRegistration(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,7 +181,7 @@ func (a *WebAuthnAuthenticator) RemoveCredential(
|
||||
}
|
||||
|
||||
user.RemoveCredential(credentialIdBase64)
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func (a *WebAuthnAuthenticator) UpdateCredential(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err = a.users.UpdateUserInternal(ctx, user)
|
||||
user, err = a.users.UpdateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ const TopicUserEnabled = "user:enabled"
|
||||
const TopicInterfaceCreated = "interface:created"
|
||||
const TopicInterfaceUpdated = "interface:updated"
|
||||
const TopicInterfaceDeleted = "interface:deleted"
|
||||
const TopicInterfaceStatsUpdated = "interface:stats:updated"
|
||||
|
||||
// endregion interface-events
|
||||
|
||||
@@ -38,7 +37,6 @@ const TopicPeerUpdated = "peer:updated"
|
||||
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
||||
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
||||
const TopicPeerStateChanged = "peer:state:changed"
|
||||
const TopicPeerStatsUpdated = "peer:stats:updated"
|
||||
|
||||
// endregion peer-events
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/mail"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/config"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
@@ -72,7 +71,7 @@ func NewMailManager(
|
||||
users UserDatabaseRepo,
|
||||
wg WireguardDatabaseRepo,
|
||||
) (*Manager, error) {
|
||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle, cfg.Mail.TemplatesPath)
|
||||
tplHandler, err := newTemplateHandler(cfg.Web.ExternalUrl, cfg.Web.SiteTitle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize template handler: %w", err)
|
||||
}
|
||||
@@ -102,15 +101,29 @@ func (m Manager) SendPeerEmail(ctx context.Context, linkOnly bool, style string,
|
||||
}
|
||||
|
||||
if peer.UserIdentifier == "" {
|
||||
return fmt.Errorf("peer %s has no user linked, no email is sent", peerId)
|
||||
slog.Debug("skipping peer email",
|
||||
"peer", peerId,
|
||||
"reason", "no user linked")
|
||||
continue
|
||||
}
|
||||
|
||||
email, user := m.resolveEmail(ctx, peer)
|
||||
if email == "" {
|
||||
return fmt.Errorf("peer %s has no valid email address, no email is sent", peerId)
|
||||
user, err := m.users.GetUser(ctx, peer.UserIdentifier)
|
||||
if err != nil {
|
||||
slog.Debug("skipping peer email",
|
||||
"peer", peerId,
|
||||
"reason", "unable to fetch user",
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = m.sendPeerEmail(ctx, linkOnly, style, &user, peer)
|
||||
if user.Email == "" {
|
||||
slog.Debug("skipping peer email",
|
||||
"peer", peerId,
|
||||
"reason", "user has no mail address")
|
||||
continue
|
||||
}
|
||||
|
||||
err = m.sendPeerEmail(ctx, linkOnly, style, user, peer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send peer email for %s: %w", peerId, err)
|
||||
}
|
||||
@@ -181,37 +194,3 @@ func (m Manager) sendPeerEmail(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Manager) resolveEmail(ctx context.Context, peer *domain.Peer) (string, domain.User) {
|
||||
user, err := m.users.GetUser(ctx, peer.UserIdentifier)
|
||||
if err != nil {
|
||||
if m.cfg.Mail.AllowPeerEmail {
|
||||
_, err := mail.ParseAddress(string(peer.UserIdentifier)) // test if the user identifier is a valid email address
|
||||
if err == nil {
|
||||
slog.Debug("peer email: using user-identifier as email",
|
||||
"peer", peer.Identifier, "email", peer.UserIdentifier)
|
||||
return string(peer.UserIdentifier), domain.User{}
|
||||
} else {
|
||||
slog.Debug("peer email: skipping peer email",
|
||||
"peer", peer.Identifier,
|
||||
"reason", "peer has no user linked and user-identifier is not a valid email address")
|
||||
return "", domain.User{}
|
||||
}
|
||||
} else {
|
||||
slog.Debug("peer email: skipping peer email",
|
||||
"peer", peer.Identifier,
|
||||
"reason", "user has no user linked")
|
||||
return "", domain.User{}
|
||||
}
|
||||
}
|
||||
|
||||
if user.Email == "" {
|
||||
slog.Debug("peer email: skipping peer email",
|
||||
"peer", peer.Identifier,
|
||||
"reason", "user has no mail address")
|
||||
return "", domain.User{}
|
||||
}
|
||||
|
||||
slog.Debug("peer email: using user email", "peer", peer.Identifier, "email", user.Email)
|
||||
return user.Email, *user
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@ import (
|
||||
"fmt"
|
||||
htmlTemplate "html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
@@ -26,50 +22,15 @@ type TemplateHandler struct {
|
||||
textTemplates *template.Template
|
||||
}
|
||||
|
||||
func newTemplateHandler(portalUrl, portalName string, basePath string) (*TemplateHandler, error) {
|
||||
// Always parse embedded defaults first
|
||||
func newTemplateHandler(portalUrl, portalName string) (*TemplateHandler, error) {
|
||||
htmlTemplateCache, err := htmlTemplate.New("Html").ParseFS(TemplateFiles, "tpl_files/*.gohtml")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse embedded html template files: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse html template files: %w", err)
|
||||
}
|
||||
|
||||
txtTemplateCache, err := template.New("Txt").ParseFS(TemplateFiles, "tpl_files/*.gotpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse embedded text template files: %w", err)
|
||||
}
|
||||
|
||||
// If a basePath is provided, ensure existence, populate if empty, then parse to override
|
||||
if basePath != "" {
|
||||
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create templates base directory %s: %w", basePath, err)
|
||||
}
|
||||
|
||||
hasTemplates, err := dirHasTemplates(basePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inspect templates directory: %w", err)
|
||||
}
|
||||
|
||||
// If no templates present, copy embedded defaults to directory
|
||||
if !hasTemplates {
|
||||
if err := copyEmbeddedTemplates(basePath); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate templates directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse files from basePath to override embedded ones.
|
||||
// Only parse when matches exist to allow partial overrides without errors.
|
||||
if matches, _ := filepath.Glob(filepath.Join(basePath, "*.gohtml")); len(matches) > 0 {
|
||||
slog.Debug("parsing html email templates from base path", "base-path", basePath, "files", matches)
|
||||
if htmlTemplateCache, err = htmlTemplateCache.ParseFiles(matches...); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse html templates from base path: %w", err)
|
||||
}
|
||||
}
|
||||
if matches, _ := filepath.Glob(filepath.Join(basePath, "*.gotpl")); len(matches) > 0 {
|
||||
slog.Debug("parsing text email templates from base path", "base-path", basePath, "files", matches)
|
||||
if txtTemplateCache, err = txtTemplateCache.ParseFiles(matches...); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse text templates from base path: %w", err)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse text template files: %w", err)
|
||||
}
|
||||
|
||||
handler := &TemplateHandler{
|
||||
@@ -82,71 +43,24 @@ func newTemplateHandler(portalUrl, portalName string, basePath string) (*Templat
|
||||
return handler, nil
|
||||
}
|
||||
|
||||
// dirHasTemplates checks whether directory contains any .gohtml or .gotpl files.
|
||||
func dirHasTemplates(basePath string) (bool, error) {
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := filepath.Ext(e.Name())
|
||||
if ext == ".gohtml" || ext == ".gotpl" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// copyEmbeddedTemplates writes embedded templates into basePath.
|
||||
func copyEmbeddedTemplates(basePath string) error {
|
||||
list, err := fs.ReadDir(TemplateFiles, "tpl_files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range list {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
// Only copy known template extensions
|
||||
if ext := filepath.Ext(name); ext != ".gohtml" && ext != ".gotpl" {
|
||||
continue
|
||||
}
|
||||
data, err := TemplateFiles.ReadFile(filepath.Join("tpl_files", name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := filepath.Join(basePath, name)
|
||||
if err := os.WriteFile(out, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigMail returns the text and html template for the mail with a link.
|
||||
func (c TemplateHandler) GetConfigMail(user *domain.User, link string) (io.Reader, io.Reader, error) {
|
||||
var tplBuff bytes.Buffer
|
||||
var htmlTplBuff bytes.Buffer
|
||||
|
||||
err := c.textTemplates.ExecuteTemplate(&tplBuff, "mail_with_link.gotpl", map[string]any{
|
||||
"User": user,
|
||||
"Link": link,
|
||||
"PortalUrl": c.portalUrl,
|
||||
"PortalName": c.portalName,
|
||||
"User": user,
|
||||
"Link": link,
|
||||
"PortalUrl": c.portalUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gotpl: %w", err)
|
||||
}
|
||||
|
||||
err = c.htmlTemplates.ExecuteTemplate(&htmlTplBuff, "mail_with_link.gohtml", map[string]any{
|
||||
"User": user,
|
||||
"Link": link,
|
||||
"PortalUrl": c.portalUrl,
|
||||
"PortalName": c.portalName,
|
||||
"User": user,
|
||||
"Link": link,
|
||||
"PortalUrl": c.portalUrl,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute template mail_with_link.gohtml: %w", err)
|
||||
|
||||
@@ -144,6 +144,8 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
},
|
||||
Identifier: domain.UserIdentifier(oldUser.Email),
|
||||
Email: oldUser.Email,
|
||||
Source: domain.UserSource(oldUser.Source),
|
||||
ProviderName: "",
|
||||
IsAdmin: oldUser.IsAdmin,
|
||||
Firstname: oldUser.Firstname,
|
||||
Lastname: oldUser.Lastname,
|
||||
@@ -157,25 +159,11 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||
LockedReason: "",
|
||||
LinkedPeerCount: 0,
|
||||
}
|
||||
|
||||
if err := newDb.Create(&newUser).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
||||
}
|
||||
|
||||
authentication := domain.UserAuthentication{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: oldUser.CreatedAt,
|
||||
UpdatedAt: oldUser.UpdatedAt,
|
||||
},
|
||||
UserIdentifier: domain.UserIdentifier(oldUser.Email),
|
||||
Source: domain.UserSource(oldUser.Source),
|
||||
ProviderName: "", // unknown
|
||||
}
|
||||
if err := newDb.Create(&authentication).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate user-authentication %s: %w", oldUser.Email, err)
|
||||
}
|
||||
|
||||
slog.Debug("user migrated successfully", "identifier", newUser.Identifier)
|
||||
}
|
||||
|
||||
@@ -358,6 +346,8 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
},
|
||||
Identifier: domain.UserIdentifier(oldPeer.Email),
|
||||
Email: oldPeer.Email,
|
||||
Source: domain.UserSourceDatabase,
|
||||
ProviderName: "",
|
||||
IsAdmin: false,
|
||||
Locked: &now,
|
||||
LockedReason: domain.DisabledReasonMigrationDummy,
|
||||
@@ -368,21 +358,6 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
||||
return fmt.Errorf("failed to migrate dummy user %s: %w", oldPeer.Email, err)
|
||||
}
|
||||
|
||||
authentication := domain.UserAuthentication{
|
||||
BaseModel: domain.BaseModel{
|
||||
CreatedBy: domain.CtxSystemV1Migrator,
|
||||
UpdatedBy: domain.CtxSystemV1Migrator,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserIdentifier: domain.UserIdentifier(oldPeer.Email),
|
||||
Source: domain.UserSourceDatabase,
|
||||
ProviderName: "", // unknown
|
||||
}
|
||||
if err := newDb.Create(&authentication).Error; err != nil {
|
||||
return fmt.Errorf("failed to migrate dummy user-authentication %s: %w", oldPeer.Email, err)
|
||||
}
|
||||
|
||||
slog.Debug("dummy user migrated successfully", "identifier", user.Identifier)
|
||||
}
|
||||
newPeer := domain.Peer{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user