Merge branch 'master' into stable

This commit is contained in:
Christoph Haas 2025-05-13 20:25:27 +02:00
commit 18296673d7
No known key found for this signature in database
22 changed files with 956 additions and 234 deletions

View File

@ -4,7 +4,7 @@ on:
pull_request:
branches: [master]
push:
branches: [master, stable, legacy]
branches: [master]
# Publish vX.X.X tags as releases.
tags: ["v*.*.*"]
@ -64,12 +64,12 @@ jobs:
# major and major.minor tags are not available for alpha or beta releases
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern=v{{major}}
# add v{{major}} tag, even for beta or release-canidate releases
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# add {{major}} tag, even for beta releases or release-canidate releases
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
# set latest tag for default branch
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v6

View File

@ -27,7 +27,14 @@ jobs:
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
- name: Publish documentation
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
run: mike deploy --push ${{ github.ref_name }}
env:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
- name: Publish latest documentation
if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
env:
GIT_COMMITTER_NAME: "github-actions[bot]"
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"

View File

@ -72,7 +72,8 @@ auth:
auth:
oidc:
# a sample Entra ID provider with environment variable substitution
# A sample Entra ID provider with environment variable substitution.
# Only users with an @outlook.com email address are allowed to register or login.
- id: azure
provider_name: azure
display_name: Login with</br>Entra ID
@ -80,6 +81,8 @@ auth:
base_url: "https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0"
client_id: "${AZURE_CLIENT_ID}"
client_secret: "${AZURE_CLIENT_SECRET}"
allowed_domains:
- "outlook.com"
extra_scopes:
- profile
- email

View File

@ -368,6 +368,10 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- **Default:** *(empty)*
- **Description:** A list of additional OIDC scopes (e.g., `profile`, `email`).
#### `allowed_domains`
- **Default:** *(empty)*
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OIDC claims to WireGuard Portal user fields.
@ -437,6 +441,10 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- **Default:** *(empty)*
- **Description:** A list of OAuth scopes.
#### `allowed_domains`
- **Default:** *(empty)*
- **Description:** A list of allowlisted domains. Only users with email addresses in these domains can log in or register. This is useful for restricting access to specific organizations or groups.
#### `field_map`
- **Default:** *(empty)*
- **Description:** Maps OAuth attributes to WireGuard Portal fields.

View File

@ -110,31 +110,33 @@ WireGuard Portal supports managing WireGuard interfaces through three distinct d
## Image Versioning
All images are hosted on Docker Hub at [https://hub.docker.com/r/wgportal/wg-portal](https://hub.docker.com/r/wgportal/wg-portal) or in the [GitHub Container Registry](https://github.com/h44z/wg-portal/pkgs/container/wg-portal).
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
There are three types of tags in the repository:
#### Semantic versioned tags
For example, `2.0.0-rc.1` or `v2.0.0-rc.1`.
These are official releases of WireGuard Portal. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases).
These are official releases of WireGuard Portal. For production deployments of WireGuard Portal, we strongly recommend using one of these versioned tags instead of the latest or canary tags.
Once these tags show up in this repository, they will never change.
There are different types of these tags:
For production deployments of WireGuard Portal, we strongly recommend using one of these tags, e.g. `wgportal/wg-portal:2.0.0`, instead of the latest or canary tags.
- Major version tags: `v2` or `2`. These tags always refer to the latest image for WireGuard Portal version **2**.
- Minor version tags: `v2.x` or `2.0`. These tags always refer to the latest image for WireGuard Portal version **2.x**.
- Specific version tags (patch version): `v2.0.0` or `2.0.0`. These tags denote a very specific release. They correspond to the GitHub tags that we make, and you can see the release notes for them here: [https://github.com/h44z/wg-portal/releases](https://github.com/h44z/wg-portal/releases). Once these tags for a specific version show up in the Docker repository, they will never change.
If you only want to stay at the same major or major+minor version, use either `v[MAJOR]` or `[MAJOR].[MINOR]` tags. For example `v2` or `2.0`.
#### The `latest` tag
Version **2** is the current stable release. Version **1** has moved to legacy status and is no longer recommended.
The lastest tag is the latest stable release of WireGuard Portal. For version **2**, this is the same as the `v2` tag.
#### latest
#### The `master` tag
This is the most recent build to master! It changes a lot and is very unstable.
This is the most recent build to the main branch! It changes a lot and is very unstable.
We recommend that you don't use it except for development purposes.
We recommend that you don't use it except for development purposes or to test the latest features.
#### Branch tags
For each commit in the master and the stable branch, a corresponding Docker image is build. These images use the `master` or `stable` tags.
## Configuration

View File

@ -692,6 +692,7 @@ paths:
tags:
- Interfaces
put:
description: This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleUpdatePut
parameters:
- description: The interface identifier.
@ -739,6 +740,7 @@ paths:
- Interfaces
/interface/new:
post:
description: This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleCreatePost
parameters:
- description: The interface data.
@ -779,6 +781,34 @@ paths:
summary: Create a new interface record.
tags:
- Interfaces
/interface/prepare:
get:
description: This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
operationId: interfaces_handlePrepareGet
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Interface'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new interface record.
tags:
- Interfaces
/metrics/by-interface/{id}:
get:
operationId: metrics_handleMetricsForInterfaceGet
@ -967,7 +997,7 @@ paths:
tags:
- Peers
put:
description: Only admins can update existing records.
description: Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
operationId: peers_handleUpdatePut
parameters:
- description: The peer identifier.
@ -1078,7 +1108,7 @@ paths:
- Peers
/peer/new:
post:
description: Only admins can create new records.
description: Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
operationId: peers_handleCreatePost
parameters:
- description: The peer data.
@ -1119,6 +1149,48 @@ paths:
summary: Create a new peer record.
tags:
- Peers
/peer/prepare/{id}:
get:
description: This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
operationId: peers_handlePrepareGet
parameters:
- description: The interface identifier.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Peer'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new peer record for the given WireGuard interface.
tags:
- Peers
/provisioning/data/peer-config:
get:
description: Normal users can only access their own record. Admins can access all records.

View File

@ -24,7 +24,7 @@
<div id="toasts"></div>
<!-- main application -->
<div id="app"></div>
<div id="app" class="d-flex flex-column flex-grow-1"></div>
<!-- vue teleport will add modals and dialogs here -->
<div id="modals"></div>

View File

@ -140,6 +140,7 @@ const currentYear = ref(new Date().getFullYear())
</div>
</div>
</div>
</footer></template>
</footer>
</template>
<style></style>

View File

@ -20,14 +20,14 @@
"delete": "Eliminar"
},
"login": {
"headline": "Por favor, inicie sessão",
"headline": "Por favor, inicie a sessão",
"username": {
"label": "Nome de utilizador",
"placeholder": "Introduza o seu nome de utilizador"
"placeholder": "Por favor, insira o seu nome de utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Introduza a sua palavra-passe"
"placeholder": "Por favor, insira a sua palavra-passe"
},
"button": "Iniciar sessão"
},
@ -40,143 +40,513 @@
"settings": "Definições",
"audit": "Registo de Auditoria",
"login": "Iniciar Sessão",
"logout": "Terminar Sessão"
"logout": "Terminar Sessão",
"keygen": "Gerador de Chave"
},
"home": {
"title": "Início",
"card": {
"interfaces": "Interfaces",
"users": "Utilizadores"
"headline": "WireGuard® Portal VPN",
"info-headline": "Mais Informações",
"abstract": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração. O seu objetivo é ser mais rápida, simples, leve e útil que o IPsec, enquanto evita grandes dores de cabeça. Pretende ser consideravelmente mais eficiente que o OpenVPN.",
"installation": {
"box-header": "Instalação do WireGuard",
"headline": "Instalação",
"content": "As instruções de instalação para o software cliente podem ser encontradas no site oficial do WireGuard.",
"button": "Abrir Instruções"
},
"about-wg": {
"box-header": "Sobre o WireGuard",
"headline": "Sobre",
"content": "WireGuard® é uma VPN extremamente simples, mas rápida e moderna que utiliza criptografia de última geração.",
"button": "Mais"
},
"about-portal": {
"box-header": "Sobre o WireGuard Portal",
"headline": "WireGuard Portal",
"content": "WireGuard Portal é um portal web de configuração simples para o WireGuard.",
"button": "Mais"
},
"profiles": {
"headline": "Perfis VPN",
"abstract": "Pode aceder e baixar as suas configurações pessoais de VPN através do seu Perfil de Utilizador.",
"content": "Para encontrar todos os seus perfis configurados, clique no botão abaixo.",
"button": "Abrir meu perfil"
},
"admin": {
"headline": "Área de Administração",
"abstract": "Na área de administração, pode gerir os peers do WireGuard, a interface do servidor e os utilizadores que têm permissão para aceder ao Portal WireGuard.",
"content": "",
"button-admin": "Abrir Administração do Servidor",
"button-user": "Abrir Administração de Utilizadores"
}
},
"interfaces": {
"title": "Interfaces",
"create": "Criar Interface",
"name": "Nome",
"address": "Endereço",
"listen-port": "Porta de Escuta",
"public-key": "Chave Pública",
"private-key": "Chave Privada",
"actions": "Ações",
"delete-dialog": {
"title": "Eliminar Interface",
"text": "Tem a certeza de que deseja eliminar a interface '{{name}}'?"
"headline": "Administração de Interfaces",
"headline-peers": "Peers VPN Atuais",
"headline-endpoints": "Endpoints Atuais",
"no-interface": {
"default-selection": "Nenhuma interface disponível",
"headline": "Nenhuma interface encontrada...",
"abstract": "Clique no botão + acima para criar uma nova interface WireGuard."
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza um nome exclusivo para a interface"
},
"address": {
"label": "Endereço",
"placeholder": "Introduza um endereço válido (ex: 10.0.0.1/24)"
},
"listen-port": {
"label": "Porta de Escuta",
"placeholder": "Introduza a porta onde o WireGuard irá escutar (ex: 51820)"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "Será gerada automaticamente se não for fornecida"
}
}
"no-peer": {
"headline": "Nenhum peer disponível",
"abstract": "Atualmente, não há peers disponíveis para a interface WireGuard selecionada."
},
"table-heading": {
"name": "Nome",
"user": "Utilizador",
"ip": "IPs",
"endpoint": "Endpoint",
"status": "Status"
},
"interface": {
"headline": "Status da interface para",
"mode": "modo",
"key": "Chave Pública",
"endpoint": "Endpoint Público",
"port": "Porta de Escuta",
"peers": "Peers Ativados",
"total-peers": "Total de Peers",
"endpoints": "Endpoints Ativados",
"total-endpoints": "Total de Endpoints",
"ip": "Endereço IP",
"default-allowed-ip": "IPs permitidos por padrão",
"dns": "Servidores DNS",
"mtu": "MTU",
"default-keep-alive": "Intervalo de Keepalive Padrão",
"button-show-config": "Mostrar configuração",
"button-download-config": "Baixar configuração",
"button-store-config": "Armazenar configuração para wg-quick",
"button-edit": "Editar interface"
},
"button-add-interface": "Adicionar Interface",
"button-add-peer": "Adicionar Peer",
"button-add-peers": "Adicionar Vários Peers",
"button-show-peer": "Mostrar Peer",
"button-edit-peer": "Editar Peer",
"peer-disabled": "Peer desativado, razão:",
"peer-expiring": "Peer expira em",
"peer-connected": "Conectado",
"peer-not-connected": "Não Conectado",
"peer-handshake": "Último handshake:"
},
"users": {
"title": "Utilizadores",
"create": "Criar Utilizador",
"name": "Nome",
"email": "Email",
"enabled": "Ativo",
"is-admin": "Administrador",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Utilizador",
"text": "Tem a certeza de que deseja eliminar o utilizador '{{nome}}'?"
"headline": "Administração de Utilizadores",
"table-heading": {
"id": "ID",
"email": "E-Mail",
"firstname": "Primeiro Nome",
"lastname": "Último Nome",
"source": "Fonte",
"peers": "Peers",
"admin": "Administrador"
},
"form": {
"name": {
"label": "Nome",
"placeholder": "Introduza o nome do utilizador"
"no-user": {
"headline": "Nenhum utilizador disponível",
"abstract": "Atualmente, não há utilizadores registados no Portal WireGuard."
},
"button-add-user": "Adicionar Utilizador",
"button-show-user": "Mostrar Utilizador",
"button-edit-user": "Editar Utilizador",
"user-disabled": "Utilizador desativado, razão:",
"user-locked": "Conta bloqueada, razão:",
"admin": "O utilizador tem privilégios de administrador",
"no-admin": "O utilizador não tem privilégios de administrador"
},
"profile": {
"headline": "Os Meus Peers VPN",
"table-heading": {
"name": "Nome",
"ip": "IPs",
"stats": "Status",
"interface": "Interface do Servidor"
},
"no-peer": {
"headline": "Nenhum peer disponível",
"abstract": "Atualmente, não há peers associados ao seu perfil de utilizador."
},
"peer-connected": "Conectado",
"button-add-peer": "Adicionar Peer",
"button-show-peer": "Mostrar Peer",
"button-edit-peer": "Editar Peer"
},
"settings": {
"headline": "Definições",
"abstract": "Aqui pode alterar suas Definições pessoais.",
"api": {
"headline": "Definições da API",
"abstract": "Aqui pode configurar as definições da API RESTful.",
"active-description": "A API está atualmente ativa para a sua conta de utilizador. Todos os pedidos para a API são autenticadas com Basic Auth. Use as seguintes credenciais para autenticação.",
"inactive-description": "A API está atualmente inativa. Pressione o botão abaixo para ativá-la.",
"user-label": "Nome de utilizador API:",
"user-placeholder": "O utilizador da API",
"token-label": "Senha da API:",
"token-placeholder": "O token da API",
"token-created-label": "Acesso API concedido em: ",
"button-disable-title": "Desativar API, invalidando o token atual.",
"button-disable-text": "Desativar API",
"button-enable-title": "Ativar API, gerando um novo token.",
"button-enable-text": "Ativar API",
"api-link": "Documentação da API"
}
},
"audit": {
"headline": "Registo de Auditoria",
"abstract": "Aqui pode encontrar o registo de auditoria de todas as ações realizadas no WireGuard Portal.",
"no-entries": {
"headline": "Nenhuma entrada no registo",
"abstract": "Atualmente, não há entradas de registo de auditoria gravadas."
},
"entries-headline": "Entradas do Registo",
"table-heading": {
"id": "#",
"time": "Hora",
"user": "Utilizador",
"severity": "Gravidade",
"origin": "Origem",
"message": "Mensagem"
}
},
"keygen": {
"headline": "Gerador de Chaves WireGuard",
"abstract": "Gere novas chaves WireGuard. As chaves são geradas no seu browser e nunca são enviadas para o servidor.",
"headline-keypair": "Novo Par de Chaves",
"headline-preshared-key": "Nova Chave Pré-Partilhada",
"button-generate": "Gerar",
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada"
},
"public-key": {
"label": "Chave Pública",
"placeholder": "A chave pública"
},
"preshared-key": {
"label": "Chave Pré-Partilhada",
"placeholder": "A chave pré-partilhada"
}
},
"modals": {
"user-view": {
"headline": "Conta de Utilizador:",
"tab-user": "Informação",
"tab-peers": "Peers",
"headline-info": "Informação do Utilizador:",
"headline-notes": "Notas:",
"email": "E-Mail",
"firstname": "Primeiro Nome",
"lastname": "Último Nome",
"phone": "Número de Telefone",
"department": "Departamento",
"api-enabled": "Acesso API",
"disabled": "Conta Desativada",
"locked": "Conta Bloqueada",
"no-peers": "O utilizador não tem peers associados.",
"peers": {
"name": "Nome",
"interface": "Interface",
"ip": "IP's"
}
},
"user-edit": {
"headline-edit": "Editar utilizador:",
"headline-new": "Novo utilizador",
"header-general": "Geral",
"header-personal": "Informação do Utilizador",
"header-notes": "Notas",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "O identificador único do utilizador"
},
"email": {
"label": "Email",
"placeholder": "Introduza o email do utilizador"
"source": {
"label": "Fonte",
"placeholder": "A fonte do utilizador"
},
"password": {
"label": "Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
"placeholder": "Uma palavra-passe super secreta",
"description": "Deixe este campo em branco para manter a palavra-passe atual."
},
"is-admin": {
"label": "Administrador"
"email": {
"label": "Email",
"placeholder": "O endereço de e-mail"
},
"enabled": {
"label": "Ativo"
"phone": {
"label": "Telefone",
"placeholder": "O número de telefone"
},
"department": {
"label": "Departamento",
"placeholder": "O departamento"
},
"firstname": {
"label": "Primeiro Nome",
"placeholder": "Primeiro Nome"
},
"lastname": {
"label": "Último Nome",
"placeholder": "Último Nome"
},
"notes": {
"label": "Notas",
"placeholder": ""
},
"disabled": {
"label": "Desativado (sem conexão WireGuard e login possível)"
},
"locked": {
"label": "Bloqueado (sem login possível, as conexões WireGuard ainda funcionam)"
},
"admin": {
"label": "É Administrador"
}
}
},
"peers": {
"title": "Peers",
"create": "Criar Peer",
"public-key": "Chave Pública",
"preshared-key": "Chave Pré-partilhada",
"endpoint": "Endpoint",
"allowed-ips": "IPs Permitidos",
"latest-handshake": "Último Handshake",
"transfer-rx": "Recebido",
"transfer-tx": "Enviado",
"persistent-keepalive": "Keepalive Persistente",
"actions": "Ações",
"edit": "Editar",
"delete-dialog": {
"title": "Eliminar Peer",
"text": "Tem a certeza de que deseja eliminar este peer?"
},
"form": {
"interface-view": {
"headline": "Configuração para a Interface:"
},
"interface-edit": {
"headline-edit": "Editar Interface:",
"headline-new": "Nova Interface",
"tab-interface": "Interface",
"tab-peerdef": "Padrões de Peer",
"header-general": "Geral",
"header-network": "Rede",
"header-crypto": "Criptografia",
"header-hooks": "Hooks da Interface",
"header-peer-hooks": "Hooks",
"header-state": "Estado",
"identifier": {
"label": "Identificador",
"placeholder": "O identificador único da interface"
},
"mode": {
"label": "Modo da Interface",
"server": "Modo Servidor",
"client": "Modo Cliente",
"any": "Modo Desconhecido"
},
"display-name": {
"label": "Nome de Exibição",
"placeholder": "O nome descritivo para a interface"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada"
},
"public-key": {
"label": "Chave Pública",
"placeholder": "Introduza a chave pública do peer"
"placeholder": "A chave pública"
},
"ip": {
"label": "Endereços IP",
"placeholder": "Endereços IP (formato CIDR)"
},
"listen-port": {
"label": "Porta de Escuta",
"placeholder": "A porta de escuta"
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Os servidores DNS que devem ser usados"
},
"dns-search": {
"label": "Domínios de Pesquisa DNS",
"placeholder": "Prefixos de pesquisa DNS"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU da interface (0 = manter o valor padrão)"
},
"firewall-mark": {
"label": "Marca de Firewall",
"placeholder": "Marca de firewall aplicada ao tráfego de saída. (0 = automático)"
},
"routing-table": {
"label": "Tabela de Roteamento",
"placeholder": "O ID da tabela de roteamento",
"description": "Casos especiais: off = não gerenciar rotas, 0 = automático"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"disabled": {
"label": "Interface Desativada"
},
"save-config": {
"label": "Guardar configuração wg-quick automaticamente"
},
"defaults": {
"endpoint": {
"label": "Endereço do Endpoint",
"placeholder": "Endereço do Endpoint",
"description": "O endereço do endpoint ao qual os peers se irão conectar. (ex. wg.exemplo.com ou wg.exemplo.com:51820)"
},
"networks": {
"label": "Redes IP",
"placeholder": "Endereços de Rede",
"description": "Os peers irão obter endereços IP a partir dessas sub-redes."
},
"allowed-ip": {
"label": "Endereços IP Permitidos",
"placeholder": "Endereços IP Permitidos por padrão"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU do cliente (0 = manter o valor padrão)"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive persistente (0 = padrão)"
}
},
"button-apply-defaults": "Aplicar Padrões de Peer"
},
"peer-view": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"section-info": "Informação do Peer",
"section-status": "Estado Atual",
"section-config": "Configuração",
"identifier": "Identificador",
"ip": "Endereços IP",
"user": "Utilizador Associado",
"notes": "Notas",
"expiry-status": "Expira em",
"disabled-status": "Desativado em",
"traffic": "Tráfego",
"connection-status": "Estatísticas de Conexão",
"upload": "Bytes Enviados (do Servidor para o Peer)",
"download": "Bytes Recebidos (do Peer para o Servidor)",
"pingable": "É Pingável",
"handshake": "Último Handshake",
"connected-since": "Conectado desde",
"endpoint": "Endpoint",
"button-download": "Baixar configuração",
"button-email": "Enviar configuração por E-Mail"
},
"peer-edit": {
"headline-edit-peer": "Editar peer:",
"headline-edit-endpoint": "Editar endpoint:",
"headline-new-peer": "Criar peer",
"headline-new-endpoint": "Criar endpoint",
"header-general": "Geral",
"header-network": "Rede",
"header-crypto": "Criptografia",
"header-hooks": "Hooks (Executados no Peer)",
"header-state": "Estado",
"display-name": {
"label": "Nome de Exibição",
"placeholder": "O nome descritivo para o peer"
},
"linked-user": {
"label": "Utilizador Associado",
"placeholder": "A conta de utilizador que possui este peer"
},
"private-key": {
"label": "Chave Privada",
"placeholder": "A chave privada",
"help": "A chave privada é armazenada de forma segura no servidor. Se o utilizador já tiver uma cópia, pode omitir este campo. O servidor ainda funciona exclusivamente com a chave pública do peer."
},
"public-key": {
"label": "Chave Pública",
"placeholder": "A chave pública"
},
"preshared-key": {
"label": "Chave Pré-partilhada",
"placeholder": "Opcional: Chave partilhada adicional para maior segurança"
"label": "Chave Pré-Partilhada",
"placeholder": "Chave pré-partilhada opcional"
},
"endpoint-public-key": {
"label": "Chave Pública do Endpoint",
"placeholder": "A chave pública do endpoint remoto"
},
"endpoint": {
"label": "Endpoint",
"placeholder": "Endereço público do peer (ex: 1.2.3.4:51820)"
"label": "Endereço do Endpoint",
"placeholder": "O endereço do endpoint remoto"
},
"allowed-ips": {
"label": "IPs Permitidos",
"placeholder": "Lista de IPs (ex: 10.0.0.2/32, 192.168.1.0/24)"
"ip": {
"label": "Endereços IP",
"placeholder": "Endereços IP (formato CIDR)"
},
"persistent-keepalive": {
"label": "Keepalive Persistente",
"placeholder": "Ex: 25 (em segundos)"
"allowed-ip": {
"label": "Endereços IP Permitidos",
"placeholder": "Endereços IP permitidos"
},
"extra-allowed-ip": {
"label": "Endereços IP adicionais permitidos",
"placeholder": "IPs adicionais permitidos (lado do servidor)",
"description": "Esses IPs serão adicionados à interface WireGuard remota como IPs permitidos."
},
"dns": {
"label": "Servidor DNS",
"placeholder": "Os servidores DNS que devem ser utilizados"
},
"dns-search": {
"label": "Domínios de Pesquisa DNS",
"placeholder": "Prefixos de pesquisa DNS"
},
"keep-alive": {
"label": "Intervalo de Keep Alive",
"placeholder": "Keepalive persistente (0 = padrão)"
},
"mtu": {
"label": "MTU",
"placeholder": "O MTU do cliente (0 = manter o padrão)"
},
"pre-up": {
"label": "Pre-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-up": {
"label": "Post-Up",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"pre-down": {
"label": "Pre-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"post-down": {
"label": "Post-Down",
"placeholder": "Um ou vários comandos bash separados por ;"
},
"disabled": {
"label": "Peer Desativado"
},
"ignore-global": {
"label": "Ignorar definições globais"
},
"expires-at": {
"label": "Data de expiração"
}
},
"peer-multi-create": {
"headline-peer": "Criar múltiplos peers",
"headline-endpoint": "Criar múltiplos endpoints",
"identifiers": {
"label": "Identificadores de utilizador",
"placeholder": "Identificadores de utilizador",
"description": "Um identificador de utilizador (nome de utilizador) para o qual um peer deve ser criado."
},
"prefix": {
"headline-peer": "Peer:",
"headline-endpoint": "Endpoint:",
"label": "Prefixo do nome exibido",
"placeholder": "O prefixo",
"description": "Um prefixo que será adicionado ao nome exibido do peer."
}
}
},
"settings": {
"title": "Definições",
"password": {
"label": "Nova Palavra-passe",
"placeholder": "Deixe em branco para manter a atual"
},
"save": "Guardar Alterações"
},
"audit": {
"title": "Registo de Auditoria",
"username": "Utilizador",
"ip": "Endereço IP",
"method": "Método",
"path": "Caminho",
"status": "Estado",
"timestamp": "Data/Hora"
},
"errors": {
"required": "Este campo é obrigatório",
"invalid-email": "Endereço de email inválido",
"invalid-address": "Endereço inválido",
"invalid-endpoint": "Endpoint inválido",
"invalid-allowed-ips": "Formato de IPs Permitidos inválido"
}
}

View File

@ -129,7 +129,7 @@ export const profileStore = defineStore('profile', {
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
.then(this.setUser)
.catch(error => {
this.setPeers([])
this.fetching = false
console.log("Failed to activate API for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",
@ -143,7 +143,7 @@ export const profileStore = defineStore('profile', {
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
.then(this.setUser)
.catch(error => {
this.setPeers([])
this.fetching = false
console.log("Failed to deactivate API for ", currentUser, ": ", error)
notify({
title: "Backend Connection Failure",

15
go.mod
View File

@ -16,19 +16,19 @@ require (
github.com/stretchr/testify v1.10.0
github.com/swaggo/swag v1.16.4
github.com/vardius/message-bus v1.1.5
github.com/vishvananda/netlink v1.3.0
github.com/vishvananda/netlink v1.3.1
github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
golang.org/x/crypto v0.37.0
golang.org/x/oauth2 v0.29.0
golang.org/x/sys v0.32.0
golang.org/x/crypto v0.38.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.33.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
gorm.io/driver/sqlserver v1.5.4
gorm.io/gorm v1.25.12
gorm.io/gorm v1.26.1
)
require (
@ -62,7 +62,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@ -82,8 +81,8 @@ require (
github.com/yeqown/reedsolomon v1.0.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/protobuf v1.36.6 // indirect

102
go.sum
View File

@ -44,24 +44,16 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
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=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
@ -83,8 +75,6 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
@ -98,12 +88,10 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -117,8 +105,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=
github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
@ -179,16 +165,10 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.6.1 h1:EQukUOma9YFZRPe4DGSscxUf9LH07rpqwisNWjSZrgU=
github.com/prometheus-community/pro-bing v0.6.1/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ=
github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA=
github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
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.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
@ -218,9 +198,8 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
@ -237,25 +216,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
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.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.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -268,28 +238,18 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
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=
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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -303,16 +263,11 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -320,13 +275,9 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@ -337,18 +288,12 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -356,8 +301,6 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -378,35 +321,26 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA=
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/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.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=

View File

@ -118,6 +118,7 @@
"BasicAuth": []
}
],
"description": "This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).",
"produces": [
"application/json"
],
@ -250,6 +251,7 @@
"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"
],
@ -309,6 +311,50 @@
}
}
},
"/interface/prepare": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).",
"produces": [
"application/json"
],
"tags": [
"Interfaces"
],
"summary": "Prepare a new interface record.",
"operationId": "interfaces_handlePrepareGet",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Interface"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/metrics/by-interface/{id}": {
"get": {
"security": [
@ -547,7 +593,7 @@
"BasicAuth": []
}
],
"description": "Only admins can update existing records.",
"description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
"produces": [
"application/json"
],
@ -779,7 +825,7 @@
"BasicAuth": []
}
],
"description": "Only admins can create new records.",
"description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).",
"produces": [
"application/json"
],
@ -839,6 +885,71 @@
}
}
},
"/peer/prepare/{id}": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.",
"produces": [
"application/json"
],
"tags": [
"Peers"
],
"summary": "Prepare a new peer record for the given WireGuard interface.",
"operationId": "peers_handlePrepareGet",
"parameters": [
{
"type": "string",
"description": "The interface identifier.",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Peer"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/provisioning/data/peer-config": {
"get": {
"security": [

View File

@ -748,6 +748,8 @@ paths:
tags:
- Interfaces
put:
description: This endpoint updates an existing interface with the provided data.
All required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleUpdatePut
parameters:
- description: The interface identifier.
@ -795,6 +797,8 @@ paths:
- Interfaces
/interface/new:
post:
description: This endpoint creates a new interface with the provided data. All
required fields must be filled (e.g. name, private key, public key, ...).
operationId: interfaces_handleCreatePost
parameters:
- description: The interface data.
@ -835,6 +839,35 @@ paths:
summary: Create a new interface record.
tags:
- Interfaces
/interface/prepare:
get:
description: This endpoint returns a new interface with default values (fresh
key pair, valid name, new IP address pool, ...).
operationId: interfaces_handlePrepareGet
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Interface'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new interface record.
tags:
- Interfaces
/metrics/by-interface/{id}:
get:
operationId: metrics_handleMetricsForInterfaceGet
@ -1024,7 +1057,8 @@ paths:
tags:
- Peers
put:
description: Only admins can update existing records.
description: Only admins can update existing records. The peer record must contain
all required fields (e.g., public key, allowed IPs).
operationId: peers_handleUpdatePut
parameters:
- description: The peer identifier.
@ -1136,7 +1170,8 @@ paths:
- Peers
/peer/new:
post:
description: Only admins can create new records.
description: Only admins can create new records. The peer record must contain
all required fields (e.g., public key, allowed IPs).
operationId: peers_handleCreatePost
parameters:
- description: The peer data.
@ -1177,6 +1212,49 @@ paths:
summary: Create a new peer record.
tags:
- Peers
/peer/prepare/{id}:
get:
description: This endpoint is used to prepare a new peer record. The returned
data contains a fresh key pair and valid ip address.
operationId: peers_handlePrepareGet
parameters:
- description: The interface identifier.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Peer'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Prepare a new peer record for the given WireGuard interface.
tags:
- Peers
/provisioning/data/peer-config:
get:
description: Normal users can only access their own record. Admins can access

View File

@ -11,6 +11,7 @@ import (
type InterfaceServiceInterfaceManagerRepo interface {
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
PrepareInterface(ctx context.Context) (*domain.Interface, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
@ -60,6 +61,19 @@ func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdenti
return interfaceData, interfacePeers, nil
}
func (s InterfaceService) Prepare(ctx context.Context) (*domain.Interface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
interfaceData, err := s.interfaces.PrepareInterface(ctx)
if err != nil {
return nil, err
}
return interfaceData, nil
}
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err

View File

@ -13,6 +13,7 @@ type PeerServicePeerManagerRepo interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
@ -95,6 +96,19 @@ func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*do
return peer, nil
}
func (s PeerService) Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
peer, err := s.peers.PreparePeer(ctx, id)
if err != nil {
return nil, err
}
return peer, nil
}
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err

View File

@ -15,6 +15,7 @@ import (
type InterfaceEndpointInterfaceService interface {
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
Prepare(context.Context) (*domain.Interface, error)
Create(context.Context, *domain.Interface) (*domain.Interface, error)
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
Delete(context.Context, domain.InterfaceIdentifier) error
@ -49,6 +50,7 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup.HandleFunc("GET /all", e.handleAllGet())
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())
@ -112,11 +114,38 @@ func (e InterfaceEndpoint) handleByIdGet() http.HandlerFunc {
}
}
// handlePrepareGet returns a gorm handler function.
//
// @ID interfaces_handlePrepareGet
// @Tags Interfaces
// @Summary Prepare a new interface record.
// @Description This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).
// @Produce json
// @Success 200 {object} models.Interface
// @Failure 401 {object} models.Error
// @Failure 403 {object} models.Error
// @Failure 500 {object} models.Error
// @Router /interface/prepare [get]
// @Security BasicAuth
func (e InterfaceEndpoint) handlePrepareGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
iface, err := e.interfaces.Prepare(r.Context())
if err != nil {
status, model := ParseServiceError(err)
respond.JSON(w, status, model)
return
}
respond.JSON(w, http.StatusOK, models.NewInterface(iface, nil))
}
}
// handleCreatePost returns a gorm handler function.
//
// @ID interfaces_handleCreatePost
// @Tags Interfaces
// @Summary Create a new interface record.
// @Description This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
// @Param request body models.Interface true "The interface data."
// @Produce json
// @Success 200 {object} models.Interface
@ -155,6 +184,7 @@ func (e InterfaceEndpoint) handleCreatePost() http.HandlerFunc {
// @ID interfaces_handleUpdatePut
// @Tags Interfaces
// @Summary Update an interface record.
// @Description This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).
// @Param id path string true "The interface identifier."
// @Param request body models.Interface true "The interface data."
// @Produce json

View File

@ -16,6 +16,7 @@ type PeerService interface {
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
Create(context.Context, *domain.Peer) (*domain.Peer, error)
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
Delete(context.Context, domain.PeerIdentifier) error
@ -51,6 +52,7 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
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("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())
@ -156,12 +158,48 @@ func (e PeerEndpoint) handleByIdGet() http.HandlerFunc {
}
}
// handlePrepareGet returns a gorm handler function.
//
// @ID peers_handlePrepareGet
// @Tags Peers
// @Summary Prepare a new peer record for the given WireGuard interface.
// @Description This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.
// @Param id path string true "The interface identifier."
// @Produce json
// @Success 200 {object} models.Peer
// @Failure 400 {object} models.Error
// @Failure 401 {object} models.Error
// @Failure 403 {object} models.Error
// @Failure 404 {object} models.Error
// @Failure 500 {object} models.Error
// @Router /peer/prepare/{id} [get]
// @Security BasicAuth
func (e PeerEndpoint) handlePrepareGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := request.Path(r, "id")
if id == "" {
respond.JSON(w, http.StatusBadRequest,
models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
return
}
peer, err := e.peers.Prepare(r.Context(), domain.InterfaceIdentifier(id))
if err != nil {
status, model := ParseServiceError(err)
respond.JSON(w, status, model)
return
}
respond.JSON(w, http.StatusOK, models.NewPeer(peer))
}
}
// handleCreatePost returns a gorm handler function.
//
// @ID peers_handleCreatePost
// @Tags Peers
// @Summary Create a new peer record.
// @Description Only admins can create new records.
// @Description Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).
// @Param request body models.Peer true "The peer data."
// @Produce json
// @Success 200 {object} models.Peer
@ -200,7 +238,7 @@ func (e PeerEndpoint) handleCreatePost() http.HandlerFunc {
// @ID peers_handleUpdatePut
// @Tags Peers
// @Summary Update a peer record.
// @Description Only admins can update existing records.
// @Description Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).
// @Param id path string true "The peer identifier."
// @Param request body models.Peer true "The peer data."
// @Produce json

View File

@ -63,6 +63,8 @@ type AuthenticatorOauth interface {
ParseUserInfo(raw map[string]any) (*domain.AuthenticatorUserInfo, error)
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
RegistrationEnabled() bool
// GetAllowedDomains returns the list of whitelisted domains
GetAllowedDomains() []string
}
// AuthenticatorLdap is the interface for all LDAP authenticators.
@ -392,6 +394,23 @@ func (a *Authenticator) randString(nByte int) (string, error) {
return base64.RawURLEncoding.EncodeToString(b), nil
}
func isDomainAllowed(email string, allowedDomains []string) bool {
if len(allowedDomains) == 0 {
return true
}
parts := strings.Split(email, "@")
if len(parts) != 2 {
return false
}
domain := strings.ToLower(parts[1])
for _, allowed := range allowedDomains {
if domain == strings.ToLower(allowed) {
return true
}
}
return false
}
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
// fetching the user information.
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
@ -431,6 +450,10 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
return nil, fmt.Errorf("unable to process user information: %w", err)
}
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
return nil, fmt.Errorf("user is not in allowed domains: %w", err)
}
if user.IsLocked() || user.IsDisabled() {
a.bus.Publish(app.TopicAuditLoginFailed, domain.AuditEventWrapper[audit.AuthEvent]{
Ctx: ctx,

View File

@ -27,6 +27,7 @@ type PlainOauthAuthenticator struct {
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
allowedDomains []string
}
func newPlainOauthAuthenticator(
@ -56,6 +57,7 @@ func newPlainOauthAuthenticator(
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
provider.allowedDomains = cfg.AllowedDomains
return provider, nil
}
@ -65,6 +67,10 @@ func (p PlainOauthAuthenticator) GetName() string {
return p.name
}
func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
return p.allowedDomains
}
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
return p.registrationEnabled

View File

@ -24,6 +24,7 @@ type OidcAuthenticator struct {
userAdminMapping *config.OauthAdminMapping
registrationEnabled bool
userInfoLogging bool
allowedDomains []string
}
func newOidcAuthenticator(
@ -57,6 +58,7 @@ func newOidcAuthenticator(
provider.userAdminMapping = &cfg.AdminMapping
provider.registrationEnabled = cfg.RegistrationEnabled
provider.userInfoLogging = cfg.LogUserInfo
provider.allowedDomains = cfg.AllowedDomains
return provider, nil
}
@ -66,6 +68,10 @@ func (o OidcAuthenticator) GetName() string {
return o.name
}
func (o OidcAuthenticator) GetAllowedDomains() []string {
return o.allowedDomains
}
// RegistrationEnabled returns whether registration is enabled for this authenticator.
func (o OidcAuthenticator) RegistrationEnabled() bool {
return o.registrationEnabled

View File

@ -188,6 +188,9 @@ type OpenIDConnectProvider struct {
// ExtraScopes specifies optional requested permissions.
ExtraScopes []string `yaml:"extra_scopes"`
// AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`
@ -226,6 +229,9 @@ type OAuthProvider struct {
// Scope specifies optional requested permissions.
Scopes []string `yaml:"scopes"`
// AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"`