Compare commits

..

27 Commits

Author SHA1 Message Date
Christoph Haas
cc472216b4 Merge branch 'master' into stable 2026-03-22 22:34:48 +01:00
Christoph
95394628d3 Merge branch 'master' into stable 2026-03-03 17:02:52 +01:00
Christoph Haas
b553375c43 Merge branch 'master' into stable 2026-02-28 22:32:43 +01:00
htiryaki-oe24
0a8ec71b3f Fixes minor chart issues #629 (#630)
(cherry picked from commit 3e0ffec07c)
2026-02-24 22:41:28 +01:00
h44z
fe4485037a Merge commit from fork 2026-02-24 22:40:13 +01:00
Arnaud Rocher
6e47d8c3e9 fix: parity of Base64/URL encoding between frontend and backend (#611)
Signed-off-by: Arnaud Rocher <arnaud.roche3@gmail.com>
(cherry picked from commit 5d58df8a19)
2026-01-29 22:39:33 +01:00
h44z
eb28492539 Merge commit from fork
* fix: prevent open redirect in OAuth return URL validation

* reformat check

---------

Co-authored-by: Arne Cools <arne.cools@intigriti.com>
(cherry picked from commit e62db0d62e)
2026-01-29 22:38:42 +01:00
Christoph
d1a4ddde10 Merge branch 'master' into stable 2025-11-23 21:00:12 +01:00
Christoph Haas
b1637b0c4e Merge branch 'master' into stable
# Conflicts:
#	internal/domain/peer.go
2025-10-19 13:25:07 +02:00
h44z
0cc7ebb83e ensure hooks run after restart (#494) (#497)
(cherry picked from commit 99df4ca3cd)
2025-09-03 22:48:45 +02:00
h44z
eb6a787cfc ensure that LDAP filter values are escaped (#512)
(cherry picked from commit 0cbca61c15)
2025-09-03 22:47:40 +02:00
Christoph Haas
b546eec4ed fix multi-peer generation, fix prefix handling (#491)
(cherry picked from commit c20f17cddf)
2025-08-12 21:25:48 +02:00
h44z
9be2133220 fix migration tool (#495) (#496)
(cherry picked from commit 9884d8c002)
2025-08-12 21:23:30 +02:00
Christoph Haas
b05837b2d9 ensure that v2 (or just 2) tags are only published for stable releases (#493)
(cherry picked from commit b099e8abfa)
2025-08-12 21:23:28 +02:00
Christoph Haas
08c8f8eac0 backport username display bugfix (#456) 2025-06-12 19:11:25 +02:00
Christoph Haas
d864e24145 improve logging of OAuth login issues, decrease auth-code exchange timeout (#451)
(cherry picked from commit e3b65ca337)
2025-06-12 19:07:46 +02:00
Christoph Haas
5b56e58fe9 fix self-provisioned peer-generation (#452)
(cherry picked from commit 61d8aa6589)
2025-06-09 17:41:29 +02:00
Christoph Haas
930ef7b573 Merge branch 'master' into stable 2025-05-16 09:58:14 +02:00
Christoph Haas
18296673d7 Merge branch 'master' into stable 2025-05-13 20:25:27 +02:00
Christoph Haas
4ccc59c109 Merge branch 'master' into stable
# Conflicts:
#	.github/workflows/docker-publish.yml
#	README.md
#	assets/tpl/admin_index.html
#	assets/tpl/user_index.html
#	cmd/wg-portal/main.go
#	docker-compose.yml
#	go.mod
#	go.sum
#	internal/common/util.go
#	internal/server/docs/docs.go
#	internal/server/handlers_common.go
#	internal/server/server.go
#	internal/wireguard/peermanager.go
2025-05-04 20:38:55 +02:00
onyx-flame
e6b01a9903 Feature (v1): add latest handshake data to API response (#203)
* feature: updated handshake-related fields type

* feature: updated handshake representations in templates

* feature: added handshake field to Swagger schema
2023-12-23 12:56:52 +01:00
Christoph Haas
2f79dd04c0 adopt github actions 2023-10-26 11:29:34 +02:00
Christoph Haas
e5ed9736b3 update docker build settings, move to new docker hub repository, use stable branch and major version tags 2023-10-26 11:22:58 +02:00
Christoph Haas
c8353b85ae Merge branch 'replace_ext_lib' into stable 2023-10-26 10:40:06 +02:00
Christoph Haas
6142031387 update gin 2023-10-26 10:39:01 +02:00
Christoph Haas
dd86d0ff49 replace inaccessible external lib 2023-10-26 10:31:29 +02:00
Christoph Haas
bdd426a679 populate peer device type (#170) 2023-10-26 10:20:08 +02:00
63 changed files with 2273 additions and 3301 deletions

View File

@@ -1,8 +1,7 @@
# Go parameters # Go parameters
GOCMD=go GOCMD=go
GOVERSION=1.25
MODULENAME=github.com/h44z/wg-portal MODULENAME=github.com/h44z/wg-portal
GOFILES=$(shell go list ./... | grep -v /vendor/) GOFILES:=$(shell go list ./... | grep -v /vendor/)
BUILDDIR=dist BUILDDIR=dist
BINARIES=$(subst cmd/,,$(wildcard cmd/*)) BINARIES=$(subst cmd/,,$(wildcard cmd/*))
IMAGE=h44z/wg-portal IMAGE=h44z/wg-portal
@@ -52,11 +51,6 @@ format:
.PHONY: test .PHONY: test
test: test-vet test-race test: test-vet test-race
#> test-in-docker: Run tests in Docker (for non-Linux environments e.g. MacOS)
.PHONY: test-in-docker
test-in-docker:
docker run --rm -u $(shell id -u):$(shell id -g) -e HOME=/tmp -v $(PWD):/app -w /app golang:$(GOVERSION) make test
#< test-vet: Static code analysis #< test-vet: Static code analysis
.PHONY: test-vet .PHONY: test-vet
test-vet: build-dependencies test-vet: build-dependencies

View File

@@ -6,9 +6,8 @@ advanced:
core: core:
admin_user: test@test.de admin_user: test@test.de
admin_password: secret admin_password: secret
create_default_peer_on_login: true create_default_peer: true
create_default_peer_on_user_creation: false create_default_peer_on_creation: false
create_default_peer_on_interface_creation: false
web: web:
external_url: http://localhost:8888 external_url: http://localhost:8888
@@ -48,7 +47,6 @@ auth:
- https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile - https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true registration_enabled: true
logout_idp_session: true
- id: oidc2 - id: oidc2
provider_name: google2 provider_name: google2
display_name: Login with</br>Google2 display_name: Login with</br>Google2
@@ -59,7 +57,6 @@ auth:
- https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile - https://www.googleapis.com/auth/userinfo.profile
registration_enabled: true registration_enabled: true
logout_idp_session: true
oauth: oauth:
- id: google_plain_oauth - id: google_plain_oauth
provider_name: google3 provider_name: google3

View File

@@ -8,7 +8,7 @@ core:
admin_password: password admin_password: password
admin_api_token: super-s3cr3t-api-token-or-a-UUID admin_api_token: super-s3cr3t-api-token-or-a-UUID
import_existing: false import_existing: false
create_default_peer_on_login: true create_default_peer: true
self_provisioning_allowed: true self_provisioning_allowed: true
backend: backend:
@@ -144,9 +144,6 @@ auth:
extra_scopes: extra_scopes:
- https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile - https://www.googleapis.com/auth/userinfo.profile
allowed_user_groups:
- the-admin-group
- vpn-users
field_map: field_map:
user_identifier: sub user_identifier: sub
email: email email: email
@@ -204,9 +201,6 @@ auth:
- email - email
- profile - profile
- i-want-some-groups - i-want-some-groups
allowed_user_groups:
- admin-group-name
- vpn-users
field_map: field_map:
email: email email: email
firstname: name firstname: name

View File

@@ -155,33 +155,17 @@ More advanced options are found in the subsequent `Advanced` section.
- **Environment Variable:** `WG_PORTAL_CORE_EDITABLE_KEYS` - **Environment Variable:** `WG_PORTAL_CORE_EDITABLE_KEYS`
- **Description:** Allow editing of WireGuard key-pairs directly in the UI. - **Description:** Allow editing of WireGuard key-pairs directly in the UI.
### `create_default_peer` (deprecated) ### `create_default_peer`
- **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER`
- **Description:** **DEPRECATED** in favor of [create_default_peer_on_login](#create_default_peer_on_login). If set to `true`, this option is equivalent to enabling `create_default_peer_on_login`. It will be removed in a future release (2.4).
### `create_default_peer_on_creation` (deprecated)
- **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION`
- **Description:** **DEPRECATED** in favor of [create_default_peer_on_user_creation](#create_default_peer_on_user_creation) and [create_default_peer_on_interface_creation](#create_default_peer_on_interface_creation). If set to `true`, both of those options are enabled. It will be removed in a future release (2.4).
### `create_default_peer_on_login`
- **Default:** `false` - **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER` - **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. - **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). - **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI).
### `create_default_peer_on_user_creation` ### `create_default_peer_on_creation`
- **Default:** `false` - **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_USER_CREATION` - **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION`
- **Description:** If a new user is created (e.g., through LDAP sync or registration) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set. - **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 is only effective for interfaces where the "Create default peer" flag is set (via the UI). - **Important:** This option requires [create_default_peer](#create_default_peer) to be enabled.
### `create_default_peer_on_interface_creation`
- **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_INTERFACE_CREATION`
- **Description:** When a new server interface is created with the "Create default peer" flag set, automatically create a default WireGuard peer on that interface for every existing user who does not yet have a peer on it.
- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI).
### `re_enable_peer_after_user_enable` ### `re_enable_peer_after_user_enable`
- **Default:** `true` - **Default:** `true`
@@ -577,10 +561,6 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- **Default:** *(empty)* - **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. - **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.
#### `allowed_user_groups`
- **Default:** *(empty)*
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
#### `field_map` #### `field_map`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** Maps OIDC claims to WireGuard Portal user fields. - **Description:** Maps OIDC claims to WireGuard Portal user fields.
@@ -616,10 +596,6 @@ Below are the properties for each OIDC provider entry inside `auth.oidc`:
- **Description:** If `true`, sensitive OIDC user data, such as tokens and raw responses, will be logged at the trace level upon login (for debugging). - **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. - **Important:** Keep this setting disabled in production environments! Remove logs once you finished debugging authentication issues.
#### `logout_idp_session`
- **Default:** `true`
- **Description:** If `true` (default), WireGuard Portal will redirect the user to the OIDC provider's `end_session_endpoint` after local logout, terminating the session at the IdP as well. Set to `false` to only invalidate the local WireGuard Portal session without touching the IdP session.
--- ---
### OAuth ### OAuth
@@ -663,10 +639,6 @@ Below are the properties for each OAuth provider entry inside `auth.oauth`:
- **Default:** *(empty)* - **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. - **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.
#### `allowed_user_groups`
- **Default:** *(empty)*
- **Description:** A list of allowlisted user groups. If configured, at least one entry in the mapped `user_groups` claim must match one of these values.
#### `field_map` #### `field_map`
- **Default:** *(empty)* - **Default:** *(empty)*
- **Description:** Maps OAuth attributes to WireGuard Portal fields. - **Description:** Maps OAuth attributes to WireGuard Portal fields.

View File

@@ -66,40 +66,6 @@ auth:
- "outlook.com" - "outlook.com"
``` ```
#### Limiting Login to Specific User Groups
You can limit the login to specific user groups by setting the `allowed_user_groups` property for OAuth2 or OIDC providers.
If this property is not empty, the user's `user_groups` claim must contain at least one matching group.
To use this feature, ensure your group claim is mapped via `field_map.user_groups`.
```yaml
auth:
oidc:
- provider_name: "oidc1"
# ... other settings
allowed_user_groups:
- "wg-users"
- "wg-admins"
field_map:
user_groups: "groups"
```
If `allowed_user_groups` is configured and the authenticated user has no matching group in `user_groups`, login is denied.
Minimal deny-by-group example:
```yaml
auth:
oauth:
- provider_name: "oauth1"
# ... other settings
allowed_user_groups:
- "vpn-users"
field_map:
user_groups: "groups"
```
#### Limit Login to Existing Users #### 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. You can limit the login to existing users only by setting the `registration_enabled` property to `false` for OAuth2 or OIDC providers.

View File

@@ -14,7 +14,7 @@
let WGPORTAL_SITE_TITLE="WireGuard Portal"; let WGPORTAL_SITE_TITLE="WireGuard Portal";
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal"; let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
</script> </script>
<script src="/api/v0/config/frontend.js" vite-ignore></script> <script src="/api/v0/config/frontend.js"></script>
</head> </head>
<body class="d-flex flex-column min-vh-100"> <body class="d-flex flex-column min-vh-100">
<noscript> <noscript>

File diff suppressed because it is too large Load Diff

View File

@@ -9,28 +9,28 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/nunito-sans": "^5.2.7", "@fontsource/nunito-sans": "^5.2.7",
"@fortawesome/fontawesome-free": "^7.2.0", "@fortawesome/fontawesome-free": "^7.1.0",
"@kyvg/vue3-notification": "^3.4.2", "@kyvg/vue3-notification": "^3.4.2",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/browser": "^13.2.2",
"@vojtechlanka/vue-tags-input": "^3.1.2", "@vojtechlanka/vue-tags-input": "^3.1.1",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootswatch": "^5.3.8", "bootswatch": "^5.3.8",
"cidr-tools": "^11.3.2", "cidr-tools": "^11.0.3",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"ip-address": "^10.1.0", "ip-address": "^10.1.0",
"is-cidr": "^6.0.3", "is-cidr": "^6.0.1",
"is-ip": "^5.0.1", "is-ip": "^5.0.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"vue": "^3.5.32", "vue": "^3.5.25",
"vue-i18n": "^11.3.2", "vue-i18n": "^11.2.2",
"vue-prism-component": "github:h44z/vue-prism-component", "vue-prism-component": "github:h44z/vue-prism-component",
"vue-router": "^5.0.4" "vue-router": "^4.6.3"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.2",
"sass-embedded": "^1.99.0", "sass-embedded": "^1.93.3",
"vite": "^8.0.8" "vite": "^7.2.7"
} }
} }

View File

@@ -53,7 +53,6 @@ const formData = ref(freshInterface())
const isSaving = ref(false) const isSaving = ref(false)
const isDeleting = ref(false) const isDeleting = ref(false)
const isApplyingDefaults = ref(false) const isApplyingDefaults = ref(false)
const isCreatingDefaultPeers = ref(false)
const isBackendValid = computed(() => { const isBackendValid = computed(() => {
if (!props.visible || !selectedInterface.value) { if (!props.visible || !selectedInterface.value) {
@@ -314,42 +313,8 @@ async function applyPeerDefaults() {
} }
} }
async function createDefaultPeers() {
if (props.interfaceId==='#NEW#') {
return; // do nothing for new interfaces
}
if (!formData.value.CreateDefaultPeer) {
return; // only allowed if the interface flag is set
}
if (isCreatingDefaultPeers.value) return
isCreatingDefaultPeers.value = true
try {
await interfaces.CreateDefaultPeers(selectedInterface.value.Identifier)
notify({
title: "Default Peers Created",
text: "Created default peers for all users on this interface.",
type: 'success',
})
await peers.LoadPeers(selectedInterface.value.Identifier) // reload peers list
} catch (e) {
console.log(e)
notify({
title: "Failed to create default peers!",
text: e.toString(),
type: 'error',
})
} finally {
isCreatingDefaultPeers.value = false
}
}
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.interface-edit.confirm-delete', {id: selectedInterface.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await interfaces.DeleteInterface(selectedInterface.value.Identifier) await interfaces.DeleteInterface(selectedInterface.value.Identifier)
@@ -524,16 +489,10 @@ async function del() {
<input v-model="formData.Disabled" class="form-check-input" type="checkbox"> <input v-model="formData.Disabled" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label> <label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
</div> </div>
<div class="d-flex align-items-center justify-content-between" v-if="formData.Mode==='server' && settings.Setting('CreateDefaultPeer')"> <div class="form-check form-switch" v-if="formData.Mode==='server' && settings.Setting('CreateDefaultPeer')">
<div class="form-check form-switch mb-0">
<input v-model="formData.CreateDefaultPeer" class="form-check-input" type="checkbox"> <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> <label class="form-check-label">{{ $t('modals.interface-edit.create-default-peer.label') }}</label>
</div> </div>
<button v-if="props.interfaceId!=='#NEW#'" class="btn btn-primary btn-sm" type="button" @click.prevent="createDefaultPeers" :disabled="!formData.CreateDefaultPeer || isCreatingDefaultPeers">
<span v-if="isCreatingDefaultPeers" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
{{ $t('modals.interface-edit.button-create-default-peers') }}
</button>
</div>
<div class="form-check form-switch" v-if="formData.Backend==='local'"> <div class="form-check form-switch" v-if="formData.Backend==='local'">
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox"> <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> <label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>

View File

@@ -26,13 +26,13 @@
display:block; display:block;
} }
.modal.show { .modal.show {
opacity: 1.0; opacity: 1;
} }
.modal-backdrop { .modal-backdrop {
background-color: rgba(0,0,0,0.6) !important; background-color: rgba(0,0,0,0.6) !important;
} }
.modal-backdrop.show { .modal-backdrop.show {
opacity: 1.0 !important; opacity: 1 !important;
} }
</style> </style>

View File

@@ -1,121 +0,0 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
currentPage: {
type: Number,
required: true
},
totalCount: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
onGotoPage: {
type: Function,
required: true
},
onNextPage: {
type: Function,
required: true
},
onPrevPage: {
type: Function,
required: true
},
hasNextPage: {
type: Boolean,
required: true
},
hasPrevPage: {
type: Boolean,
required: true
}
});
const totalPages = computed(() => Math.ceil(props.totalCount / props.pageSize));
const pages = computed(() => {
const current = props.currentPage;
const last = totalPages.value;
const delta = 2; // Number of pages to show before and after current page
const range = [];
const rangeWithDots = [];
// If total pages is small, just show all pages
if (last <= 7) {
for (let i = 1; i <= last; i++) {
rangeWithDots.push({ type: 'page', value: i });
}
return rangeWithDots;
}
// Calculate the range around the current page
let start = Math.max(2, current - delta);
let end = Math.min(last - 1, current + delta);
// Adjust range to always show a consistent number of pages if possible
if (current <= delta + 2) {
end = 2 + delta * 2;
} else if (current >= last - delta - 1) {
start = last - delta * 2 - 1;
}
// Add dots before the range if needed
if (start > 2) {
rangeWithDots.push({ type: 'page', value: 1 });
rangeWithDots.push({ type: 'dots', value: 'dots-start' });
} else {
for (let i = 1; i < start; i++) {
rangeWithDots.push({ type: 'page', value: i });
}
}
// Add the central range
for (let i = start; i <= end; i++) {
rangeWithDots.push({ type: 'page', value: i });
}
// Add dots after the range if needed
if (end < last - 1) {
rangeWithDots.push({ type: 'dots', value: 'dots-end' });
rangeWithDots.push({ type: 'page', value: last });
} else {
for (let i = end + 1; i <= last; i++) {
rangeWithDots.push({ type: 'page', value: i });
}
}
return rangeWithDots;
});
</script>
<template>
<ul class="pagination pagination-sm mb-0" v-if="totalPages > 1">
<li :class="{ disabled: !hasPrevPage }" class="page-item">
<a class="page-link" href="#" @click.prevent="hasPrevPage && onPrevPage()">&laquo;</a>
</li>
<li v-for="item in pages" :key="item.type === 'page' ? item.value : item.value" :class="{ active: currentPage === item.value, disabled: item.type === 'dots' }" class="page-item">
<a v-if="item.type === 'page'" class="page-link" href="#" @click.prevent="onGotoPage(item.value)">{{ item.value }}</a>
<span v-else class="page-link">...</span>
</li>
<li :class="{ disabled: !hasNextPage }" class="page-item">
<a class="page-link" href="#" @click.prevent="hasNextPage && onNextPage()">&raquo;</a>
</li>
</ul>
</template>
<style scoped>
.page-link {
cursor: pointer;
}
.page-item.disabled .page-link {
cursor: default;
}
</style>

View File

@@ -294,7 +294,6 @@ async function save() {
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.peer-edit.confirm-delete', {id: selectedPeer.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await peers.DeletePeer(selectedPeer.value.Identifier) await peers.DeletePeer(selectedPeer.value.Identifier)

View File

@@ -114,7 +114,6 @@ async function save() {
async function del() { async function del() {
if (isDeleting.value) return if (isDeleting.value) return
if (!confirm(t('modals.user-edit.confirm-delete', {id: selectedUser.value.Identifier}))) return
isDeleting.value = true isDeleting.value = true
try { try {
await users.DeleteUser(selectedUser.value.Identifier) await users.DeleteUser(selectedUser.value.Identifier)

View File

@@ -382,8 +382,7 @@
"persist-local-changes": { "persist-local-changes": {
"label": "Lokale Änderungen speichern" "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.", "sync-warning": "Um diesen synchronisierten Benutzer zu bearbeiten, aktivieren Sie die lokale Änderungsspeicherung. Andernfalls werden Ihre Änderungen bei der nächsten Synchronisierung überschrieben."
"confirm-delete": "Benutzer '{id}' wirklich löschen?"
}, },
"interface-view": { "interface-view": {
"headline": "Konfiguration für Schnittstelle:" "headline": "Konfiguration für Schnittstelle:"
@@ -504,9 +503,7 @@
"placeholder": "Persistentes Keepalive (0 = Standard)" "placeholder": "Persistentes Keepalive (0 = Standard)"
} }
}, },
"button-apply-defaults": "Peer-Standardeinstellungen anwenden", "button-apply-defaults": "Peer-Standardeinstellungen anwenden"
"button-create-default-peers": "Standard-Peers erstellen",
"confirm-delete": "Interface '{id}' wirklich löschen?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -628,8 +625,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Ablaufdatum" "label": "Ablaufdatum"
}, }
"confirm-delete": "Peer '{id}' wirklich löschen?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Mehrere Peers erstellen", "headline-peer": "Mehrere Peers erstellen",

View File

@@ -382,8 +382,7 @@
"persist-local-changes": { "persist-local-changes": {
"label": "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.", "sync-warning": "To modify this synchronized user, enable local change persistence. Otherwise, your changes will be overwritten during the next synchronization."
"confirm-delete": "Are you sure you want to delete user '{id}'?"
}, },
"interface-view": { "interface-view": {
"headline": "Config for Interface:" "headline": "Config for Interface:"
@@ -504,9 +503,8 @@
"placeholder": "Persistent Keepalive (0 = default)" "placeholder": "Persistent Keepalive (0 = default)"
} }
}, },
"button-apply-defaults": "Apply Peer Defaults",
"button-create-default-peers": "Create Default Peers", "button-apply-defaults": "Apply Peer Defaults"
"confirm-delete": "Are you sure you want to delete interface '{id}'?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -628,8 +626,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Expiry date" "label": "Expiry date"
}, }
"confirm-delete": "Are you sure you want to delete peer '{id}'?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Create multiple peers", "headline-peer": "Create multiple peers",

View File

@@ -365,8 +365,7 @@
}, },
"admin": { "admin": {
"label": "Es administrador" "label": "Es administrador"
}, }
"confirm-delete": "Seguro que desea eliminar el usuario '{id}'?"
}, },
"interface-view": { "interface-view": {
"headline": "Configuración de la interfaz:" "headline": "Configuración de la interfaz:"
@@ -494,9 +493,7 @@
"placeholder": "Keepalive Persistente (0 = por defecto)" "placeholder": "Keepalive Persistente (0 = por defecto)"
} }
}, },
"button-apply-defaults": "Aplicar Valores Predeterminados de peers", "button-apply-defaults": "Aplicar Valores Predeterminados de peers"
"button-create-default-peers": "Crear Peers Predeterminados",
"confirm-delete": "Seguro que desea eliminar la interfaz '{id}'?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -616,8 +613,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Fecha de expiración" "label": "Fecha de expiración"
}, }
"confirm-delete": "Seguro que desea eliminar el par '{id}'?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Crear múltiples peers", "headline-peer": "Crear múltiples peers",

View File

@@ -126,7 +126,9 @@
"peer-expiring": "Le pair expire le", "peer-expiring": "Le pair expire le",
"peer-connected": "Connecté", "peer-connected": "Connecté",
"peer-not-connected": "Non connecté", "peer-not-connected": "Non connecté",
"peer-handshake": "Dernière négociation :" "peer-handshake": "Dernière négociation :",
"button-show-peer": "Afficher le pair",
"button-edit-peer": "Modifier le pair"
}, },
"users": { "users": {
"headline": "Administration des utilisateurs", "headline": "Administration des utilisateurs",
@@ -262,8 +264,7 @@
}, },
"admin": { "admin": {
"label": "Est Admin" "label": "Est Admin"
}, }
"confirm-delete": "Voulez-vous vraiment supprimer l'utilisateur \"{id}\" ?"
}, },
"interface-view": { "interface-view": {
"headline": "Configuration pour l'interface :" "headline": "Configuration pour l'interface :"
@@ -376,9 +377,7 @@
"placeholder": "Persistent Keepalive (0 = par défaut)" "placeholder": "Persistent Keepalive (0 = par défaut)"
} }
}, },
"button-apply-defaults": "Appliquer les valeurs par défaut des pairs", "button-apply-defaults": "Appliquer les valeurs par défaut des pairs"
"button-create-default-peers": "Créer les pairs par défaut",
"confirm-delete": "Voulez-vous vraiment supprimer l'interface \"{id}\" ?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Pair :", "headline-peer": "Pair :",
@@ -494,8 +493,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Date d'expiration" "label": "Date d'expiration"
}, }
"confirm-delete": "Voulez-vous vraiment supprimer le pair \"{id}\" ?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Créer plusieurs pairs", "headline-peer": "Créer plusieurs pairs",

View File

@@ -282,7 +282,6 @@
"label": "관리자 여부" "label": "관리자 여부"
} }
}, },
"confirm-delete": "사용자 '{id}'를 삭제하시겠습니까?",
"interface-view": { "interface-view": {
"headline": "인터페이스 구성:" "headline": "인터페이스 구성:"
}, },
@@ -394,9 +393,7 @@
"placeholder": "영구 Keepalive (0 = 기본값)" "placeholder": "영구 Keepalive (0 = 기본값)"
} }
}, },
"button-apply-defaults": "피어 기본값 적용", "button-apply-defaults": "피어 기본값 적용"
"button-create-default-peers": "기본 피어 생성",
"confirm-delete": "인터페이스 '{id}'를 삭제하시겠습니까?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "피어:", "headline-peer": "피어:",
@@ -512,8 +509,7 @@
}, },
"expires-at": { "expires-at": {
"label": "만료 날짜" "label": "만료 날짜"
}, }
"confirm-delete": "피어 '{id}'를 삭제하시겠습니까?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "여러 피어 생성", "headline-peer": "여러 피어 생성",

View File

@@ -300,8 +300,7 @@
}, },
"admin": { "admin": {
"label": "É Administrador" "label": "É Administrador"
}, }
"confirm-delete": "Tem certeza que deseja excluir o utilizador '{id}'?"
}, },
"interface-view": { "interface-view": {
"headline": "Configuração para a Interface:" "headline": "Configuração para a Interface:"
@@ -414,9 +413,7 @@
"placeholder": "Keepalive persistente (0 = padrão)" "placeholder": "Keepalive persistente (0 = padrão)"
} }
}, },
"button-apply-defaults": "Aplicar Padrões de Peer", "button-apply-defaults": "Aplicar Padrões de Peer"
"button-create-default-peers": "Criar Peers Padrão",
"confirm-delete": "Tem certeza que deseja excluir a interface '{id}'?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -533,8 +530,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Data de expiração" "label": "Data de expiração"
}, }
"confirm-delete": "Tem certeza que deseja excluir o par '{id}'?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Criar múltiplos peers", "headline-peer": "Criar múltiplos peers",

View File

@@ -366,8 +366,7 @@
}, },
"admin": { "admin": {
"label": "Является администратором" "label": "Является администратором"
}, }
"confirm-delete": "Вы уверены, что хотите удалить пользователя «{id}»?"
}, },
"interface-view": { "interface-view": {
"headline": "Конфигурация интерфейса:" "headline": "Конфигурация интерфейса:"
@@ -485,9 +484,7 @@
"placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)" "placeholder": "Постоянное поддержание активности (0 = значение по умолчанию)"
} }
}, },
"button-apply-defaults": "Применить настройки пира по умолчанию", "button-apply-defaults": "Применить настройки пира по умолчанию"
"button-create-default-peers": "Создать пиров по умолчанию",
"confirm-delete": "Вы уверены, что хотите удалить интерфейс «{id}»?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Пир:", "headline-peer": "Пир:",
@@ -608,8 +605,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Дата истечения срока действия" "label": "Дата истечения срока действия"
}, }
"confirm-delete": "Вы уверены, что хотите удалить пир «{id}»?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Создать несколько узлов", "headline-peer": "Создать несколько узлов",

View File

@@ -151,6 +151,7 @@
"admin": "Користувач має адміністративні привілеї", "admin": "Користувач має адміністративні привілеї",
"no-admin": "Користувач не має адміністративних привілеїв" "no-admin": "Користувач не має адміністративних привілеїв"
}, },
"profile": { "profile": {
"headline": "Мої VPN-піри", "headline": "Мої VPN-піри",
"table-heading": { "table-heading": {
@@ -188,6 +189,7 @@
"api-link": "Документація API" "api-link": "Документація API"
} }
}, },
"modals": { "modals": {
"user-view": { "user-view": {
"headline": "Обліковий запис користувача:", "headline": "Обліковий запис користувача:",
@@ -262,8 +264,7 @@
}, },
"admin": { "admin": {
"label": "Адміністратор" "label": "Адміністратор"
}, }
"confirm-delete": "Ви впевнені, що хочете видалити користувача «{id}»?"
}, },
"interface-view": { "interface-view": {
"headline": "Конфігурація для інтерфейсу:" "headline": "Конфігурація для інтерфейсу:"
@@ -376,9 +377,7 @@
"placeholder": "Постійний Keepalive (0 = за замовчуванням)" "placeholder": "Постійний Keepalive (0 = за замовчуванням)"
} }
}, },
"button-apply-defaults": "Застосувати значення за замовчуванням для пірів", "button-apply-defaults": "Застосувати значення за замовчуванням для пірів"
"button-create-default-peers": "Створити пірів за замовчуванням",
"confirm-delete": "Ви впевнені, що хочете видалити інтерфейс «{id}»?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Пір:", "headline-peer": "Пір:",
@@ -494,8 +493,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Дата закінчення терміну дії" "label": "Дата закінчення терміну дії"
}, }
"confirm-delete": "Ви впевнені, що хочете видалити пір «{id}»?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Створити декілька пір", "headline-peer": "Створити декілька пір",

View File

@@ -240,8 +240,7 @@
}, },
"admin": { "admin": {
"label": "Là Quản trị viên" "label": "Là Quản trị viên"
}, }
"confirm-delete": "Ban co chac muon xoa nguoi dung '{id}' khong?"
}, },
"interface-view": { "interface-view": {
"headline": "Cấu hình cho Giao diện:" "headline": "Cấu hình cho Giao diện:"
@@ -354,9 +353,8 @@
"placeholder": "Giữ kết nối liên tục (0 = mặc định)" "placeholder": "Giữ kết nối liên tục (0 = mặc định)"
} }
}, },
"button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer",
"button-create-default-peers": "Tạo Peer Mặc định", "button-apply-defaults": "Áp dụng Cài đặt Mặc định của Peer"
"confirm-delete": "Ban co chac muon xoa giao dien '{id}' khong?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "Peer:", "headline-peer": "Peer:",
@@ -472,8 +470,7 @@
}, },
"expires-at": { "expires-at": {
"label": "Ngày hết hạn" "label": "Ngày hết hạn"
}, }
"confirm-delete": "Ban co chac muon xoa peer '{id}' khong?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "Tạo nhiều peer", "headline-peer": "Tạo nhiều peer",

View File

@@ -242,7 +242,6 @@
"label": "管理员" "label": "管理员"
} }
}, },
"confirm-delete": "确定要删除用户“{id}”吗?",
"interface-view": { "interface-view": {
"headline": "接口配置: " "headline": "接口配置: "
}, },
@@ -354,9 +353,7 @@
"placeholder": "持久保持连接 (0 = 默认)" "placeholder": "持久保持连接 (0 = 默认)"
} }
}, },
"button-apply-defaults": "应用节点默认值", "button-apply-defaults": "应用节点默认值"
"button-create-default-peers": "创建默认节点",
"confirm-delete": "确定要删除接口“{id}”吗?"
}, },
"peer-view": { "peer-view": {
"headline-peer": "节点: ", "headline-peer": "节点: ",
@@ -472,8 +469,7 @@
}, },
"expires-at": { "expires-at": {
"label": "过期日期" "label": "过期日期"
}, }
"confirm-delete": "确定要删除对等点“{id}”吗?"
}, },
"peer-multi-create": { "peer-multi-create": {
"headline-peer": "创建多个节点", "headline-peer": "创建多个节点",

View File

@@ -1,6 +1,7 @@
import {createRouter, createWebHashHistory} from 'vue-router' import {createRouter, createWebHashHistory} from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import InterfaceView from '../views/InterfaceView.vue'
import {authStore} from '@/stores/auth' import {authStore} from '@/stores/auth'
import {securityStore} from '@/stores/security' import {securityStore} from '@/stores/security'
@@ -19,6 +20,11 @@ const router = createRouter({
name: 'login', name: 'login',
component: LoginView component: LoginView
}, },
{
path: '/interface',
name: 'interface',
component: InterfaceView
},
{ {
path: '/interfaces', path: '/interfaces',
name: 'interfaces', name: 'interfaces',

View File

@@ -11,6 +11,7 @@ export const auditStore = defineStore('audit', {
filter: "", filter: "",
pageSize: 10, pageSize: 10,
pageOffset: 0, pageOffset: 0,
pages: [],
fetching: false, fetching: false,
}), }),
getters: { getters: {
@@ -40,22 +41,33 @@ export const auditStore = defineStore('audit', {
afterPageSizeChange() { afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes // reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0 this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
}, },
gotoPage(page) { gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
}, },
nextPage() { nextPage() {
if (this.hasNextPage) {
this.pageOffset += this.pageSize this.pageOffset += this.pageSize
}
this.calculatePages()
}, },
previousPage() { previousPage() {
if (this.hasPrevPage) {
this.pageOffset -= this.pageSize this.pageOffset -= this.pageSize
}
this.calculatePages()
}, },
setEntries(entries) { setEntries(entries) {
this.entries = entries this.entries = entries
this.calculatePages()
this.fetching = false this.fetching = false
}, },
async LoadEntries() { async LoadEntries() {

View File

@@ -108,19 +108,12 @@ export const authStore = defineStore('auth',{
this.setUserInfo(null) this.setUserInfo(null)
this.ResetReturnUrl() // just to be sure^^ this.ResetReturnUrl() // just to be sure^^
let logoutResponse = null
try { try {
logoutResponse = await apiWrapper.post(`/auth/logout`) await apiWrapper.post(`/auth/logout`)
} catch (e) { } catch (e) {
console.log("Logout request failed:", e) console.log("Logout request failed:", e)
} }
const redirectUrl = logoutResponse?.RedirectUrl
if (redirectUrl) {
window.location.href = redirectUrl
return
}
notify({ notify({
title: "Logged Out", title: "Logged Out",
text: "Logout successful!", text: "Logout successful!",

View File

@@ -148,18 +148,6 @@ export const interfaceStore = defineStore('interfaces', {
throw new Error(error) throw new Error(error)
}) })
}, },
async CreateDefaultPeers(id) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/create-default-peers`)
.then(() => {
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async SaveConfiguration(id) { async SaveConfiguration(id) {
this.fetching = true this.fetching = true
return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`) return apiWrapper.post(`${baseUrl}/${base64_url_encode(id)}/save-config`)

View File

@@ -19,6 +19,7 @@ export const peerStore = defineStore('peers', {
filter: "", filter: "",
pageSize: 10, pageSize: 10,
pageOffset: 0, pageOffset: 0,
pages: [],
fetching: false, fetching: false,
sortKey: 'IsConnected', // Default sort key sortKey: 'IsConnected', // Default sort key
sortOrder: -1, // 1 for ascending, -1 for descending sortOrder: -1, // 1 for ascending, -1 for descending
@@ -86,22 +87,33 @@ export const peerStore = defineStore('peers', {
afterPageSizeChange() { afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes // reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0 this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
}, },
gotoPage(page) { gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
}, },
nextPage() { nextPage() {
if (this.hasNextPage) {
this.pageOffset += this.pageSize this.pageOffset += this.pageSize
}
this.calculatePages()
}, },
previousPage() { previousPage() {
if (this.hasPrevPage) {
this.pageOffset -= this.pageSize this.pageOffset -= this.pageSize
}
this.calculatePages()
}, },
setPeers(peers) { setPeers(peers) {
this.peers = peers this.peers = peers
this.calculatePages()
this.fetching = false this.fetching = false
this.trafficStats = {} this.trafficStats = {}
}, },

View File

@@ -20,6 +20,7 @@ export const profileStore = defineStore('profile', {
filter: "", filter: "",
pageSize: 10, pageSize: 10,
pageOffset: 0, pageOffset: 0,
pages: [],
fetching: false, fetching: false,
sortKey: 'IsConnected', // Default sort key sortKey: 'IsConnected', // Default sort key
sortOrder: -1, // 1 for ascending, -1 for descending sortOrder: -1, // 1 for ascending, -1 for descending
@@ -79,19 +80,29 @@ export const profileStore = defineStore('profile', {
afterPageSizeChange() { afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes // reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0 this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredPeerCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
}, },
gotoPage(page) { gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
}, },
nextPage() { nextPage() {
if (this.hasNextPage) {
this.pageOffset += this.pageSize this.pageOffset += this.pageSize
}
this.calculatePages()
}, },
previousPage() { previousPage() {
if (this.hasPrevPage) {
this.pageOffset -= this.pageSize this.pageOffset -= this.pageSize
}
this.calculatePages()
}, },
setPeers(peers) { setPeers(peers) {
this.peers = peers this.peers = peers

View File

@@ -12,6 +12,7 @@ export const userStore = defineStore('users', {
filter: "", filter: "",
pageSize: 10, pageSize: 10,
pageOffset: 0, pageOffset: 0,
pages: [],
fetching: false, fetching: false,
}), }),
getters: { getters: {
@@ -42,22 +43,33 @@ export const userStore = defineStore('users', {
afterPageSizeChange() { afterPageSizeChange() {
// reset pageOffset to avoid problems with new page sizes // reset pageOffset to avoid problems with new page sizes
this.pageOffset = 0 this.pageOffset = 0
this.calculatePages()
},
calculatePages() {
let pageCounter = 1;
this.pages = []
for (let i = 0; i < this.FilteredCount; i+=this.pageSize) {
this.pages.push(pageCounter++)
}
}, },
gotoPage(page) { gotoPage(page) {
this.pageOffset = (page-1) * this.pageSize this.pageOffset = (page-1) * this.pageSize
this.calculatePages()
}, },
nextPage() { nextPage() {
if (this.hasNextPage) {
this.pageOffset += this.pageSize this.pageOffset += this.pageSize
}
this.calculatePages()
}, },
previousPage() { previousPage() {
if (this.hasPrevPage) {
this.pageOffset -= this.pageSize this.pageOffset -= this.pageSize
}
this.calculatePages()
}, },
setUsers(users) { setUsers(users) {
this.users = users this.users = users
this.calculatePages()
this.fetching = false this.fetching = false
}, },
setUserPeers(peers) { setUserPeers(peers) {

View File

@@ -1,7 +1,6 @@
<script setup> <script setup>
import { onMounted } from "vue"; import { onMounted } from "vue";
import {auditStore} from "@/stores/audit"; import {auditStore} from "@/stores/audit";
import Pagination from "@/components/Pagination.vue";
const audit = auditStore() const audit = auditStore()
@@ -61,24 +60,28 @@ onMounted(async () => {
</table> </table>
</div> </div>
<hr> <hr>
<div class="mt-3">
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-6">
<Pagination <ul class="pagination pagination-sm">
:currentPage="audit.currentPage" <li :class="{disabled:audit.pageOffset===0}" class="page-item">
:totalCount="audit.FilteredCount" <a class="page-link" @click="audit.previousPage">&laquo;</a>
:pageSize="audit.pageSize" </li>
:hasNextPage="audit.hasNextPage"
:hasPrevPage="audit.hasPrevPage" <li v-for="page in audit.pages" :key="page" :class="{active:audit.currentPage===page}" class="page-item">
:onGotoPage="audit.gotoPage" <a class="page-link" @click="audit.gotoPage(page)">{{page}}</a>
:onNextPage="audit.nextPage" </li>
:onPrevPage="audit.previousPage"
/> <li :class="{disabled:!audit.hasNextPage}" class="page-item">
<a class="page-link" @click="audit.nextPage">&raquo;</a>
</li>
</ul>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-6">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-6 col-form-label text-md-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label> <label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6"> <div class="col-sm-6">
<select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @change="audit.afterPageSizeChange()"> <select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @click="audit.afterPageSizeChange()">
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
@@ -89,4 +92,5 @@ onMounted(async () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

View File

@@ -1,10 +1,9 @@
<script setup> <script setup>
import PeerViewModal from "@/components/PeerViewModal.vue"; import PeerViewModal from "../components/PeerViewModal.vue";
import PeerEditModal from "@/components/PeerEditModal.vue"; import PeerEditModal from "../components/PeerEditModal.vue";
import PeerMultiCreateModal from "@/components/PeerMultiCreateModal.vue"; import PeerMultiCreateModal from "../components/PeerMultiCreateModal.vue";
import InterfaceEditModal from "@/components/InterfaceEditModal.vue"; import InterfaceEditModal from "../components/InterfaceEditModal.vue";
import InterfaceViewModal from "@/components/InterfaceViewModal.vue"; import InterfaceViewModal from "../components/InterfaceViewModal.vue";
import Pagination from "@/components/Pagination.vue";
import {computed, onMounted, ref} from "vue"; import {computed, onMounted, ref} from "vue";
import {peerStore} from "@/stores/peers"; import {peerStore} from "@/stores/peers";
@@ -483,23 +482,26 @@ onMounted(async () => {
<hr v-if="interfaces.Count!==0"> <hr v-if="interfaces.Count!==0">
<div v-if="interfaces.Count!==0" class="mt-3"> <div v-if="interfaces.Count!==0" class="mt-3">
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-6">
<Pagination <ul class="pagination pagination-sm">
:currentPage="peers.currentPage" <li :class="{disabled:peers.pageOffset===0}" class="page-item">
:totalCount="peers.FilteredCount" <a class="page-link" @click="peers.previousPage">&laquo;</a>
:pageSize="peers.pageSize" </li>
:hasNextPage="peers.hasNextPage"
:hasPrevPage="peers.hasPrevPage" <li v-for="page in peers.pages" :key="page" :class="{active:peers.currentPage===page}" class="page-item">
:onGotoPage="peers.gotoPage" <a class="page-link" @click="peers.gotoPage(page)">{{page}}</a>
:onNextPage="peers.nextPage" </li>
:onPrevPage="peers.previousPage"
/> <li :class="{disabled:!peers.hasNextPage}" class="page-item">
<a class="page-link" @click="peers.nextPage">&raquo;</a>
</li>
</ul>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-6">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-6 col-form-label text-md-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label> <label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6"> <div class="col-sm-6">
<select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @change="peers.afterPageSizeChange()"> <select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @click="peers.afterPageSizeChange()">
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>

View File

@@ -6,7 +6,6 @@ import { useI18n } from "vue-i18n";
import { profileStore } from "@/stores/profile"; import { profileStore } from "@/stores/profile";
import { peerStore } from "@/stores/peers"; import { peerStore } from "@/stores/peers";
import UserPeerEditModal from "@/components/UserPeerEditModal.vue"; import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
import Pagination from "@/components/Pagination.vue";
import { settingsStore } from "@/stores/settings"; import { settingsStore } from "@/stores/settings";
import { humanFileSize } from "@/helpers/utils"; import { humanFileSize } from "@/helpers/utils";
@@ -67,6 +66,7 @@ onMounted(async () => {
await profile.LoadPeers() await profile.LoadPeers()
await profile.LoadStats() await profile.LoadStats()
await profile.LoadInterfaces() await profile.LoadInterfaces()
await profile.calculatePages(); // Forces to show initial page number
}) })
</script> </script>
@@ -185,25 +185,28 @@ onMounted(async () => {
<hr> <hr>
<div class="mt-3"> <div class="mt-3">
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-6">
<Pagination <ul class="pagination pagination-sm">
:currentPage="profile.currentPage" <li :class="{ disabled: profile.pageOffset === 0 }" class="page-item">
:totalCount="profile.FilteredPeerCount" <a class="page-link" @click="profile.previousPage">&laquo;</a>
:pageSize="profile.pageSize" </li>
:hasNextPage="profile.hasNextPage"
:hasPrevPage="profile.hasPrevPage" <li v-for="page in profile.pages" :key="page" :class="{ active: profile.currentPage === page }" class="page-item">
:onGotoPage="profile.gotoPage" <a class="page-link" @click="profile.gotoPage(page)">{{ page }}</a>
:onNextPage="profile.nextPage" </li>
:onPrevPage="profile.previousPage"
/> <li :class="{ disabled: !profile.hasNextPage }" class="page-item">
<a class="page-link" @click="profile.nextPage">&raquo;</a>
</li>
</ul>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-6">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-6 col-form-label text-md-end" for="paginationSelector"> <label class="col-sm-6 col-form-label text-end" for="paginationSelector">
{{ $t('general.pagination.size')}}: {{ $t('general.pagination.size')}}:
</label> </label>
<div class="col-sm-6"> <div class="col-sm-6">
<select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @change="profile.afterPageSizeChange()"> <select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @click="profile.afterPageSizeChange()">
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>

View File

@@ -1,9 +1,8 @@
<script setup> <script setup>
import {userStore} from "@/stores/users"; import {userStore} from "@/stores/users";
import {ref, onMounted, computed} from "vue"; import {ref, onMounted, computed} from "vue";
import UserEditModal from "@/components/UserEditModal.vue"; import UserEditModal from "../components/UserEditModal.vue";
import UserViewModal from "@/components/UserViewModal.vue"; import UserViewModal from "../components/UserViewModal.vue";
import Pagination from "@/components/Pagination.vue";
import {useI18n} from "vue-i18n"; import {useI18n} from "vue-i18n";
const users = userStore() const users = userStore()
@@ -166,24 +165,28 @@ onMounted(() => {
</table> </table>
</div> </div>
<hr> <hr>
<div class="mt-3">
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-6">
<Pagination <ul class="pagination pagination-sm">
:currentPage="users.currentPage" <li :class="{disabled:users.pageOffset===0}" class="page-item">
:totalCount="users.FilteredCount" <a class="page-link" @click="users.previousPage">&laquo;</a>
:pageSize="users.pageSize" </li>
:hasNextPage="users.hasNextPage"
:hasPrevPage="users.hasPrevPage" <li v-for="page in users.pages" :key="page" :class="{active:users.currentPage===page}" class="page-item">
:onGotoPage="users.gotoPage" <a class="page-link" @click="users.gotoPage(page)">{{page}}</a>
:onNextPage="users.nextPage" </li>
:onPrevPage="users.previousPage"
/> <li :class="{disabled:!users.hasNextPage}" class="page-item">
<a class="page-link" @click="users.nextPage">&raquo;</a>
</li>
</ul>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-6">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-6 col-form-label text-md-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label> <label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
<div class="col-sm-6"> <div class="col-sm-6">
<select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @change="users.afterPageSizeChange()"> <select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @click="users.afterPageSizeChange()">
<option value="10">10</option> <option value="10">10</option>
<option value="25">25</option> <option value="25">25</option>
<option value="50">50</option> <option value="50">50</option>
@@ -194,4 +197,5 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>

69
go.mod
View File

@@ -1,16 +1,16 @@
module github.com/h44z/wg-portal module github.com/h44z/wg-portal
go 1.25.7 go 1.25.0
require ( require (
github.com/a8m/envsubst v1.4.3 github.com/a8m/envsubst v1.4.3
github.com/alexedwards/scs/v2 v2.9.0 github.com/alexedwards/scs/v2 v2.9.0
github.com/coreos/go-oidc/v3 v3.18.0 github.com/coreos/go-oidc/v3 v3.17.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.13
github.com/go-pkgz/routegroup v1.6.0 github.com/go-pkgz/routegroup v1.6.0
github.com/go-playground/validator/v10 v10.30.2 github.com/go-playground/validator/v10 v10.30.1
github.com/go-webauthn/webauthn v0.16.4 github.com/go-webauthn/webauthn v0.16.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/prometheus-community/pro-bing v0.8.0 github.com/prometheus-community/pro-bing v0.8.0
@@ -22,9 +22,9 @@ require (
github.com/xhit/go-simple-mail/v2 v2.16.0 github.com/xhit/go-simple-mail/v2 v2.16.0
github.com/yeqown/go-qrcode/v2 v2.2.5 github.com/yeqown/go-qrcode/v2 v2.2.5
github.com/yeqown/go-qrcode/writer/compressed v1.0.1 github.com/yeqown/go-qrcode/writer/compressed v1.0.1
golang.org/x/crypto v0.50.0 golang.org/x/crypto v0.49.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.43.0 golang.org/x/sys v0.42.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
@@ -41,27 +41,27 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.23.0 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.4 // indirect github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.26.0 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.26.0 // indirect github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-test/deep v1.1.1 // indirect github.com/go-test/deep v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.3 // indirect github.com/go-webauthn/x v0.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
@@ -69,42 +69,41 @@ require (
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.9.1 // indirect github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/genetlink v1.4.0 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect
github.com/mdlayher/netlink v1.11.0 // indirect github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.6.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect
github.com/microsoft/go-mssqldb v1.9.8 // indirect github.com/microsoft/go-mssqldb v1.9.6 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.19.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.35.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/net v0.53.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.44.0 // indirect golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
modernc.org/libc v1.72.0 // indirect modernc.org/libc v1.68.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.48.2 // indirect modernc.org/sqlite v1.46.1 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect
) )

182
go.sum
View File

@@ -3,29 +3,29 @@ filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= 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.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= 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 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.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=
@@ -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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -48,8 +48,8 @@ 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/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
@@ -58,37 +58,37 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/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 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-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.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-openapi/jsonpointer v0.23.0 h1:c25HFTJ6uWGmoe5BQI6p72p4o7KnlWYsy1MeFlAumsw= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.23.0/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= 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.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= 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 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg= github.com/go-pkgz/routegroup v1.6.0/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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -97,18 +97,18 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= 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-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 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q= github.com/go-webauthn/webauthn v0.16.1 h1:x5/SSki5/aIfogaRukqvbg/RXa3Sgxy/9vU7UfFPHKU=
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4= github.com/go-webauthn/webauthn v0.16.1/go.mod h1:RBS+rtQJMkE5VfMQ4diDA2VNrEL8OeUhp4Srz37FHbQ=
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA= github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk=
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk= github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -143,8 +143,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc= github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -176,17 +176,17 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/genetlink v1.4.0 h1:f/Xs7Y2T+GyX9b3dbiUhnLE9InGs5F9RxJ2JwBMl71o= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.4.0/go.mod h1:d1hrKr8fwZU2JkcAtQUAzeTrI7nbgQSl+5k1cC0biSA= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.11.0 h1:Cot7ixQZL6P/pxRFB4z3jRdGPYeZosFT+WHS3sMXy8Y= github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
github.com/mdlayher/netlink v1.11.0/go.mod h1:rMwDzh42W85uW3yTtiTRZFX9uway98aDQ5i+D8Jq4g4= github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594=
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= 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.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.8 h1:d4IFMvF/o+HdpXUqbBfzHvn/NlFA75YGcfHUUvDFJEM= github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=
github.com/microsoft/go-mssqldb v1.9.8/go.mod h1:eGSRSGAW4hKMy5YcAenhCDjIRm2rhqIdmmwgciMzLus= github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
@@ -195,8 +195,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= 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 v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
@@ -211,8 +209,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -236,8 +234,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA= github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5JNfgjzpzvYLHjH0QOgPZPYnRWGA=
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
@@ -262,8 +258,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -278,16 +274,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.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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -305,8 +303,8 @@ 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.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -338,8 +336,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.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.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.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -368,16 +366,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.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= 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= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
@@ -404,20 +402,20 @@ gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQ
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 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 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU= modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0= modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= 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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -426,8 +424,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.48.2 h1:5CnW4uP8joZtA0LedVqLbZV5GD7F/0x91AXeSyjoh5c= modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.48.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -232,26 +232,28 @@ func (r *SqlRepo) migrate() error {
slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{})) slog.Debug("running migration: interface status", "result", r.db.AutoMigrate(&domain.InterfaceStatus{}))
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{})) slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
var existingSysStat SysStat existingSysStat := SysStat{}
var err error
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
// Migration: 0 --> 1 // Migration: 0 --> 1
if existingSysStat.SchemaVersion == 0 { if existingSysStat.SchemaVersion == 0 {
const schemaVersion = 1 const schemaVersion = 1
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version sysStat := SysStat{
if err != nil { MigratedAt: time.Now(),
return err 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)
} }
slog.Debug("sys-stat entry written", "schema_version", schemaVersion) slog.Debug("sys-stat entry written", "schema_version", schemaVersion)
existingSysStat = sysStat // ensure that follow-up checks test against the latest version
} }
// Migration: 1 --> 2 // Migration: 1 --> 2
if existingSysStat.SchemaVersion == 1 { if existingSysStat.SchemaVersion == 1 {
const schemaVersion = 2 const schemaVersion = 2
// Preserve existing behavior for installations that had default-peer-creation enabled. // Preserve existing behavior for installations that had default-peer-creation enabled.
if r.cfg.DefaultPeerCreationEnabled() { if r.cfg.Core.CreateDefaultPeer {
err := r.db.Model(&domain.Interface{}). err := r.db.Model(&domain.Interface{}).
Where("type = ?", domain.InterfaceTypeServer). Where("type = ?", domain.InterfaceTypeServer).
Update("create_default_peer", true).Error Update("create_default_peer", true).Error
@@ -260,10 +262,14 @@ func (r *SqlRepo) migrate() error {
} }
slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion) slog.Debug("migrated interface create_default_peer flags", "schema_version", schemaVersion)
} }
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version sysStat := SysStat{
if err != nil { MigratedAt: time.Now(),
return err 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 // Migration: 2 --> 3
@@ -301,43 +307,17 @@ func (r *SqlRepo) migrate() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to migrate to multi-auth: %w", err) return fmt.Errorf("failed to migrate to multi-auth: %w", err)
} }
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
}
// Migration: 3 --> 4
if existingSysStat.SchemaVersion == 3 {
const schemaVersion = 4
cutoff := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
// Fix zero created_at timestamps for users. Set the to the last known update timestamp.
err := r.db.Model(&domain.User{}).Where("created_at < ?", cutoff).
Update("created_at", gorm.Expr("updated_at")).Error
if err != nil {
slog.Warn("failed to fix zero created_at for users", "error", err)
}
slog.Debug("fixed zero created_at timestamps for users", "schema_version", schemaVersion)
existingSysStat, err = r.addMigration(schemaVersion) // ensure that follow-up checks test against the latest version
if err != nil {
return err
}
}
return nil
}
func (r *SqlRepo) addMigration(schemaVersion uint64) (SysStat, error) {
sysStat := SysStat{ sysStat := SysStat{
MigratedAt: time.Now(), MigratedAt: time.Now(),
SchemaVersion: schemaVersion, SchemaVersion: schemaVersion,
} }
if err := r.db.Create(&sysStat).Error; err != nil { if err := r.db.Create(&sysStat).Error; err != nil {
return SysStat{}, 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)
} }
return sysStat, nil existingSysStat = sysStat // ensure that follow-up checks test against the latest version
}
return nil
} }
// region interfaces // region interfaces
@@ -502,7 +482,7 @@ func (r *SqlRepo) getOrCreateInterface(
Identifier: id, Identifier: id,
} }
err := tx.Preload("Addresses").Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error err := tx.Attrs(interfaceDefaults).FirstOrCreate(&in, id).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -711,7 +691,7 @@ func (r *SqlRepo) getOrCreatePeer(ui *domain.ContextUserInfo, tx *gorm.DB, id do
Identifier: id, Identifier: id,
} }
err := tx.Preload("Addresses").Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error err := tx.Attrs(interfaceDefaults).FirstOrCreate(&peer, id).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,168 +0,0 @@
package adapters
import (
"context"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
require.NoError(t, err)
return db
}
func TestUpsertUser_SetsCreatedAtWhenZero(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
// CreatedAt is zero
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.False(t, user.CreatedAt.IsZero(), "CreatedAt should be set when it was zero")
assert.Equal(t, ui.UserId(), user.UpdatedBy, "UpdatedBy should be set when it was empty")
assert.WithinDuration(t, user.UpdatedAt, user.CreatedAt, time.Second,
"CreatedAt should be close to UpdatedAt for new user")
}
func TestUpsertUser_PreservesExistingCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ui := domain.SystemAdminContextUserInfo()
originalTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
user := &domain.User{
Identifier: "test-user",
Email: "test@example.com",
BaseModel: domain.BaseModel{
CreatedAt: originalTime,
CreatedBy: "original-creator",
},
}
err := repo.upsertUser(ui, db, user)
require.NoError(t, err)
assert.Equal(t, originalTime, user.CreatedAt, "CreatedAt should not be overwritten")
assert.Equal(t, "original-creator", user.CreatedBy, "CreatedBy should not be overwritten")
}
func TestSaveUser_NewUserGetsCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(&domain.User{}, &domain.UserAuthentication{}, &domain.UserWebauthnCredential{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
before := time.Now().Add(-time.Second)
err := repo.SaveUser(ctx, "new-user", func(u *domain.User) (*domain.User, error) {
u.Email = "new@example.com"
return u, nil
})
require.NoError(t, err)
var saved domain.User
require.NoError(t, db.First(&saved, "identifier = ?", "new-user").Error)
assert.False(t, saved.CreatedAt.IsZero(), "CreatedAt should not be zero")
assert.True(t, saved.CreatedAt.After(before), "CreatedAt should be recent")
assert.NotEmpty(t, saved.CreatedBy, "CreatedBy should be set")
}
func TestMigration_FixesZeroCreatedAt(t *testing.T) {
db := newTestDB(t)
// Manually create tables and seed schema version 3
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
// Insert schema versions 1, 2, 3 so migration starts at 3
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
// Insert a user with zero created_at but valid updated_at
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"zero-user", "zero@example.com", time.Time{}, updatedAt,
).Error)
// Run migration
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
// Verify created_at was backfilled from updated_at
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "zero-user").Error)
assert.Equal(t, updatedAt, user.CreatedAt, "created_at should be backfilled from updated_at")
// Verify schema version advanced to 4
var latest SysStat
require.NoError(t, db.Order("schema_version DESC").First(&latest).Error)
assert.Equal(t, uint64(4), latest.SchemaVersion)
}
func TestMigration_DoesNotTouchValidCreatedAt(t *testing.T) {
db := newTestDB(t)
require.NoError(t, db.AutoMigrate(
&SysStat{},
&domain.User{},
&domain.UserAuthentication{},
&domain.Interface{},
&domain.Cidr{},
&domain.Peer{},
&domain.AuditEntry{},
&domain.UserWebauthnCredential{},
))
for v := uint64(1); v <= 3; v++ {
require.NoError(t, db.Create(&SysStat{SchemaVersion: v, MigratedAt: time.Now()}).Error)
}
createdAt := time.Date(2024, 3, 1, 8, 0, 0, 0, time.UTC)
updatedAt := time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC)
require.NoError(t, db.Exec(
"INSERT INTO users (identifier, email, created_at, updated_at) VALUES (?, ?, ?, ?)",
"valid-user", "valid@example.com", createdAt, updatedAt,
).Error)
repo := &SqlRepo{db: db, cfg: &config.Config{}}
require.NoError(t, repo.migrate())
var user domain.User
require.NoError(t, db.First(&user, "identifier = ?", "valid-user").Error)
assert.Equal(t, createdAt, user.CreatedAt, "valid created_at should not be modified")
}

View File

@@ -1,73 +0,0 @@
package adapters
import (
"context"
"reflect"
"testing"
"github.com/glebarez/sqlite"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func init() {
schema.RegisterSerializer("encstr", dummySerializer{})
}
type dummySerializer struct{}
func (dummySerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue any) error {
return nil
}
func (dummySerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue any) (any, error) {
if fieldValue == nil {
return nil, nil
}
if v, ok := fieldValue.(string); ok {
return v, nil
}
if v, ok := fieldValue.(domain.PreSharedKey); ok {
return string(v), nil
}
return fieldValue, nil
}
func TestSqlRepo_SaveInterface_Simple(t *testing.T) {
// Initialize in-memory database
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
require.NoError(t, err)
// Migrate only what's needed for this test (avoids Peer and its encstr serializer)
require.NoError(t, db.AutoMigrate(&domain.Interface{}, &domain.Cidr{}))
repo := &SqlRepo{db: db, cfg: &config.Config{}}
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
ifaceId := domain.InterfaceIdentifier("wg0")
// 1. Create an interface with one address
addr, _ := domain.CidrFromString("10.0.0.1/24")
initialIface := &domain.Interface{
Identifier: ifaceId,
Addresses: []domain.Cidr{addr},
}
require.NoError(t, db.Create(initialIface).Error)
// 2. Perform a "partial" update using SaveInterface (this is the buggy path)
err = repo.SaveInterface(ctx, ifaceId, func(in *domain.Interface) (*domain.Interface, error) {
in.DisplayName = "New Name"
return in, nil
})
require.NoError(t, err)
// 3. Verify that the address was NOT deleted
var finalIface domain.Interface
require.NoError(t, db.Preload("Addresses").First(&finalIface, "identifier = ?", ifaceId).Error)
require.Equal(t, "New Name", finalIface.DisplayName)
require.Len(t, finalIface.Addresses, 1, "Address list should still have 1 entry!")
require.Equal(t, "10.0.0.1/24", finalIface.Addresses[0].Cidr)
}

View File

@@ -2,7 +2,6 @@ package backend
import ( import (
"context" "context"
"fmt"
"io" "io"
"github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/config"
@@ -19,7 +18,6 @@ type InterfaceServiceInterfaceManager interface {
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
PrepareInterface(ctx context.Context) (*domain.Interface, error) PrepareInterface(ctx context.Context) (*domain.Interface, error)
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
CreateDefaultPeers(ctx context.Context, id domain.InterfaceIdentifier) error
} }
type InterfaceServiceConfigFileManager interface { type InterfaceServiceConfigFileManager interface {
@@ -91,10 +89,3 @@ func (i InterfaceService) PersistInterfaceConfig(ctx context.Context, id domain.
func (i InterfaceService) ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error { func (i InterfaceService) ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error {
return i.interfaces.ApplyPeerDefaults(ctx, in) return i.interfaces.ApplyPeerDefaults(ctx, in)
} }
func (i InterfaceService) CreateDefaultPeers(ctx context.Context, id domain.InterfaceIdentifier) error {
if !i.cfg.DefaultPeerCreationEnabled() {
return fmt.Errorf("default peer creation is not enabled")
}
return i.interfaces.CreateDefaultPeers(ctx, id)
}

View File

@@ -25,9 +25,7 @@ type AuthenticationService interface {
// OauthLoginStep1 initiates the OAuth login flow. // OauthLoginStep1 initiates the OAuth login flow.
OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error) OauthLoginStep1(_ context.Context, providerId string) (authCodeUrl, state, nonce string, err error)
// OauthLoginStep2 completes the OAuth login flow and logins the user in. // OauthLoginStep2 completes the OAuth login flow and logins the user in.
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, error) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error)
// OauthProviderLogoutUrl returns an IdP logout URL for the given provider if supported.
OauthProviderLogoutUrl(providerId, idTokenHint, postLogoutRedirectUri string) (string, bool)
} }
type WebAuthnService interface { type WebAuthnService interface {
@@ -333,7 +331,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
} }
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
user, idTokenHint, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce, user, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
oauthCode) oauthCode)
cancel() cancel()
if err != nil { if err != nil {
@@ -348,7 +346,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
return return
} }
e.setAuthenticatedUser(r, user, provider, idTokenHint) e.setAuthenticatedUser(r, user)
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) { if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
queryParams := returnUrl.Query() queryParams := returnUrl.Query()
@@ -361,7 +359,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
} }
} }
func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User, oauthProvider, idTokenHint string) { func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User) {
// start a fresh session // start a fresh session
e.session.DestroyData(r.Context()) e.session.DestroyData(r.Context())
@@ -376,9 +374,8 @@ func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User, o
currentSession.OauthState = "" currentSession.OauthState = ""
currentSession.OauthNonce = "" currentSession.OauthNonce = ""
currentSession.OauthProvider = oauthProvider currentSession.OauthProvider = ""
currentSession.OauthReturnTo = "" currentSession.OauthReturnTo = ""
currentSession.OauthIdToken = idTokenHint
e.session.SetData(r.Context(), currentSession) e.session.SetData(r.Context(), currentSession)
} }
@@ -421,7 +418,7 @@ func (e AuthEndpoint) handleLoginPost() http.HandlerFunc {
return return
} }
e.setAuthenticatedUser(r, user, "", "") e.setAuthenticatedUser(r, user)
respond.JSON(w, http.StatusOK, user) respond.JSON(w, http.StatusOK, user)
} }
@@ -433,33 +430,19 @@ func (e AuthEndpoint) handleLoginPost() http.HandlerFunc {
// @Tags Authentication // @Tags Authentication
// @Summary Get all available external login providers. // @Summary Get all available external login providers.
// @Produce json // @Produce json
// @Success 200 {object} model.LogoutResponse // @Success 200 {object} model.Error
// @Router /auth/logout [post] // @Router /auth/logout [post]
func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc { func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
currentSession := e.session.GetData(r.Context()) currentSession := e.session.GetData(r.Context())
if !currentSession.LoggedIn { // Not logged in if !currentSession.LoggedIn { // Not logged in
respond.JSON(w, http.StatusOK, model.LogoutResponse{Message: "not logged in"}) respond.JSON(w, http.StatusOK, model.Error{Code: http.StatusOK, Message: "not logged in"})
return return
} }
postLogoutRedirectUri := e.cfg.Web.ExternalUrl
if e.cfg.Web.BasePath != "" {
postLogoutRedirectUri += e.cfg.Web.BasePath
}
postLogoutRedirectUri += "/#/login"
var redirectUrl *string
if currentSession.OauthProvider != "" {
if idpLogoutUrl, ok := e.authService.OauthProviderLogoutUrl(currentSession.OauthProvider,
currentSession.OauthIdToken, postLogoutRedirectUri); ok {
redirectUrl = &idpLogoutUrl
}
}
e.session.DestroyData(r.Context()) e.session.DestroyData(r.Context())
respond.JSON(w, http.StatusOK, model.LogoutResponse{Message: "logout ok", RedirectUrl: redirectUrl}) respond.JSON(w, http.StatusOK, model.Error{Code: http.StatusOK, Message: "logout ok"})
} }
} }
@@ -710,7 +693,7 @@ func (e AuthEndpoint) handleWebAuthnLoginFinish() http.HandlerFunc {
return return
} }
e.setAuthenticatedUser(r, user, "", "") e.setAuthenticatedUser(r, user)
respond.JSON(w, http.StatusOK, model.NewUser(user, false)) respond.JSON(w, http.StatusOK, model.NewUser(user, false))
} }

View File

@@ -145,7 +145,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
MinPasswordLength: e.cfg.Auth.MinPasswordLength, MinPasswordLength: e.cfg.Auth.MinPasswordLength,
AvailableBackends: controllerFn(), AvailableBackends: controllerFn(),
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin, LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
CreateDefaultPeer: e.cfg.DefaultPeerCreationEnabled(), CreateDefaultPeer: e.cfg.Core.CreateDefaultPeer,
}) })
} }
} }

View File

@@ -33,8 +33,6 @@ type InterfaceService interface {
PersistInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) error PersistInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) error
// ApplyPeerDefaults applies the peer defaults to all peers of the given interface. // ApplyPeerDefaults applies the peer defaults to all peers of the given interface.
ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
// CreateDefaultPeers creates default peers for all existing users on the given interface.
CreateDefaultPeers(ctx context.Context, id domain.InterfaceIdentifier) error
} }
type InterfaceEndpoint struct { type InterfaceEndpoint struct {
@@ -75,7 +73,6 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup.HandleFunc("GET /config/{id}", e.handleConfigGet()) apiGroup.HandleFunc("GET /config/{id}", e.handleConfigGet())
apiGroup.HandleFunc("POST /{id}/save-config", e.handleSaveConfigPost()) apiGroup.HandleFunc("POST /{id}/save-config", e.handleSaveConfigPost())
apiGroup.HandleFunc("POST /{id}/apply-peer-defaults", e.handleApplyPeerDefaultsPost()) apiGroup.HandleFunc("POST /{id}/apply-peer-defaults", e.handleApplyPeerDefaultsPost())
apiGroup.HandleFunc("POST /{id}/create-default-peers", e.handleCreateDefaultPeersPost())
apiGroup.HandleFunc("GET /peers/{id}", e.handlePeersGet()) apiGroup.HandleFunc("GET /peers/{id}", e.handlePeersGet())
} }
@@ -424,34 +421,3 @@ func (e InterfaceEndpoint) handleApplyPeerDefaultsPost() http.HandlerFunc {
respond.Status(w, http.StatusNoContent) respond.Status(w, http.StatusNoContent)
} }
} }
// handleCreateDefaultPeersPost returns a gorm Handler function.
//
// @ID interfaces_handleCreateDefaultPeersPost
// @Tags Interface
// @Summary Create default peers for all existing users on the given interface.
// @Produce json
// @Param id path string true "The interface identifier"
// @Success 204 "No content if creating the default peers was successful"
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /interface/{id}/create-default-peers [post]
func (e InterfaceEndpoint) handleCreateDefaultPeersPost() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := Base64UrlDecode(request.Path(r, "id"))
if id == "" {
respond.JSON(w, http.StatusBadRequest,
model.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
return
}
if err := e.interfaceService.CreateDefaultPeers(r.Context(), domain.InterfaceIdentifier(id)); err != nil {
respond.JSON(w, http.StatusInternalServerError, model.Error{
Code: http.StatusInternalServerError, Message: err.Error(),
})
return
}
respond.Status(w, http.StatusNoContent)
}
}

View File

@@ -30,7 +30,6 @@ type SessionData struct {
OauthNonce string OauthNonce string
OauthProvider string OauthProvider string
OauthReturnTo string OauthReturnTo string
OauthIdToken string
WebAuthnData string WebAuthnData string
@@ -90,6 +89,5 @@ func (s *SessionWrapper) defaultSessionData() SessionData {
OauthNonce: "", OauthNonce: "",
OauthProvider: "", OauthProvider: "",
OauthReturnTo: "", OauthReturnTo: "",
OauthIdToken: "",
} }
} }

View File

@@ -45,11 +45,6 @@ type OauthInitiationResponse struct {
State string State string
} }
type LogoutResponse struct {
Message string `json:"Message"`
RedirectUrl *string `json:"RedirectUrl,omitempty"`
}
type WebAuthnCredentialRequest struct { type WebAuthnCredentialRequest struct {
Name string `json:"Name"` Name string `json:"Name"`
} }

View File

@@ -65,11 +65,6 @@ type AuthenticatorOauth interface {
RegistrationEnabled() bool RegistrationEnabled() bool
// GetAllowedDomains returns the list of whitelisted domains // GetAllowedDomains returns the list of whitelisted domains
GetAllowedDomains() []string GetAllowedDomains() []string
// GetAllowedUserGroups returns the list of whitelisted user groups.
// If non-empty, at least one user group must match.
GetAllowedUserGroups() []string
// GetLogoutUrl returns an IdP logout URL if supported by the provider.
GetLogoutUrl(idTokenHint, postLogoutRedirectUri string) (string, bool)
} }
// AuthenticatorLdap is the interface for all LDAP authenticators. // AuthenticatorLdap is the interface for all LDAP authenticators.
@@ -502,63 +497,31 @@ func isDomainAllowed(email string, allowedDomains []string) bool {
return false return false
} }
func isAnyAllowedUserGroup(userGroups, allowedUserGroups []string) bool {
if len(allowedUserGroups) == 0 {
return true
}
allowed := make(map[string]struct{}, len(allowedUserGroups))
for _, group := range allowedUserGroups {
trimmed := strings.TrimSpace(group)
if trimmed == "" {
continue
}
allowed[trimmed] = struct{}{}
}
if len(allowed) == 0 {
return false
}
for _, group := range userGroups {
if _, ok := allowed[strings.TrimSpace(group)]; ok {
return true
}
}
return false
}
// OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and // OauthLoginStep2 finishes the oauth authentication flow by exchanging the code for an access token and
// fetching the user information. // fetching the user information.
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, error) { func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, error) {
oauthProvider, ok := a.oauthAuthenticators[providerId] oauthProvider, ok := a.oauthAuthenticators[providerId]
if !ok { if !ok {
return nil, "", fmt.Errorf("missing oauth provider %s", providerId) return nil, fmt.Errorf("missing oauth provider %s", providerId)
} }
oauth2Token, err := oauthProvider.Exchange(ctx, code) oauth2Token, err := oauthProvider.Exchange(ctx, code)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("unable to exchange code: %w", err) return nil, fmt.Errorf("unable to exchange code: %w", err)
} }
idTokenHint, _ := oauth2Token.Extra("id_token").(string)
rawUserInfo, err := oauthProvider.GetUserInfo(ctx, oauth2Token, nonce) rawUserInfo, err := oauthProvider.GetUserInfo(ctx, oauth2Token, nonce)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("unable to fetch user information: %w", err) return nil, fmt.Errorf("unable to fetch user information: %w", err)
} }
userInfo, err := oauthProvider.ParseUserInfo(rawUserInfo) userInfo, err := oauthProvider.ParseUserInfo(rawUserInfo)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("unable to parse user information: %w", err) return nil, fmt.Errorf("unable to parse user information: %w", err)
} }
if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) { if !isDomainAllowed(userInfo.Email, oauthProvider.GetAllowedDomains()) {
return nil, "", fmt.Errorf("user %s is not in allowed domains", userInfo.Email) return nil, fmt.Errorf("user %s is not in allowed domains", userInfo.Email)
}
if !isAnyAllowedUserGroup(userInfo.UserGroups, oauthProvider.GetAllowedUserGroups()) {
return nil, "", fmt.Errorf("user %s is not in allowed user groups", userInfo.Identifier)
} }
ctx = domain.SetUserInfo(ctx, ctx = domain.SetUserInfo(ctx,
@@ -574,7 +537,7 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
Error: err.Error(), Error: err.Error(),
}, },
}) })
return nil, "", fmt.Errorf("unable to process user information: %w", err) return nil, fmt.Errorf("unable to process user information: %w", err)
} }
if user.IsLocked() || user.IsDisabled() { if user.IsLocked() || user.IsDisabled() {
@@ -586,7 +549,7 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
Error: "user is locked", Error: "user is locked",
}, },
}) })
return nil, "", errors.New("user is locked") return nil, errors.New("user is locked")
} }
a.bus.Publish(app.TopicAuthLogin, user.Identifier) a.bus.Publish(app.TopicAuthLogin, user.Identifier)
@@ -598,16 +561,7 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
}, },
}) })
return user, idTokenHint, nil return user, nil
}
func (a *Authenticator) OauthProviderLogoutUrl(providerId, idTokenHint, postLogoutRedirectUri string) (string, bool) {
oauthProvider, ok := a.oauthAuthenticators[providerId]
if !ok {
return "", false
}
return oauthProvider.GetLogoutUrl(idTokenHint, postLogoutRedirectUri)
} }
func (a *Authenticator) processUserInfo( func (a *Authenticator) processUserInfo(

View File

@@ -29,7 +29,6 @@ type PlainOauthAuthenticator struct {
userInfoLogging bool userInfoLogging bool
sensitiveInfoLogging bool sensitiveInfoLogging bool
allowedDomains []string allowedDomains []string
allowedUserGroups []string
} }
func newPlainOauthAuthenticator( func newPlainOauthAuthenticator(
@@ -61,7 +60,6 @@ func newPlainOauthAuthenticator(
provider.userInfoLogging = cfg.LogUserInfo provider.userInfoLogging = cfg.LogUserInfo
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
provider.allowedDomains = cfg.AllowedDomains provider.allowedDomains = cfg.AllowedDomains
provider.allowedUserGroups = cfg.AllowedUserGroups
return provider, nil return provider, nil
} }
@@ -75,14 +73,6 @@ func (p PlainOauthAuthenticator) GetAllowedDomains() []string {
return p.allowedDomains return p.allowedDomains
} }
func (p PlainOauthAuthenticator) GetAllowedUserGroups() []string {
return p.allowedUserGroups
}
func (p PlainOauthAuthenticator) GetLogoutUrl(_, _ string) (string, bool) {
return "", false
}
// RegistrationEnabled returns whether registration is enabled for the OAuth authenticator. // RegistrationEnabled returns whether registration is enabled for the OAuth authenticator.
func (p PlainOauthAuthenticator) RegistrationEnabled() bool { func (p PlainOauthAuthenticator) RegistrationEnabled() bool {
return p.registrationEnabled return p.registrationEnabled

View File

@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/url"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -27,9 +26,6 @@ type OidcAuthenticator struct {
userInfoLogging bool userInfoLogging bool
sensitiveInfoLogging bool sensitiveInfoLogging bool
allowedDomains []string allowedDomains []string
allowedUserGroups []string
endSessionEndpoint string
logoutIdpSession bool
} }
func newOidcAuthenticator( func newOidcAuthenticator(
@@ -65,17 +61,6 @@ func newOidcAuthenticator(
provider.userInfoLogging = cfg.LogUserInfo provider.userInfoLogging = cfg.LogUserInfo
provider.sensitiveInfoLogging = cfg.LogSensitiveInfo provider.sensitiveInfoLogging = cfg.LogSensitiveInfo
provider.allowedDomains = cfg.AllowedDomains provider.allowedDomains = cfg.AllowedDomains
provider.allowedUserGroups = cfg.AllowedUserGroups
provider.logoutIdpSession = cfg.LogoutIdpSession == nil || *cfg.LogoutIdpSession
var providerMetadata struct {
EndSessionEndpoint string `json:"end_session_endpoint"`
}
if err = provider.provider.Claims(&providerMetadata); err != nil {
slog.Debug("OIDC: failed to parse provider metadata", "provider", cfg.ProviderName, "error", err)
} else {
provider.endSessionEndpoint = providerMetadata.EndSessionEndpoint
}
return provider, nil return provider, nil
} }
@@ -89,38 +74,6 @@ func (o OidcAuthenticator) GetAllowedDomains() []string {
return o.allowedDomains return o.allowedDomains
} }
func (o OidcAuthenticator) GetAllowedUserGroups() []string {
return o.allowedUserGroups
}
func (o OidcAuthenticator) GetLogoutUrl(idTokenHint, postLogoutRedirectUri string) (string, bool) {
if !o.logoutIdpSession {
return "", false
}
if o.endSessionEndpoint == "" {
slog.Debug("OIDC logout URL generation disabled: provider has no end_session_endpoint", "provider", o.name)
return "", false
}
logoutUrl, err := url.Parse(o.endSessionEndpoint)
if err != nil {
slog.Debug("OIDC logout URL generation failed, unable to parse end_session_endpoint url",
"provider", o.name, "error", err)
return "", false
}
params := logoutUrl.Query()
if idTokenHint != "" {
params.Set("id_token_hint", idTokenHint)
}
if postLogoutRedirectUri != "" {
params.Set("post_logout_redirect_uri", postLogoutRedirectUri)
}
logoutUrl.RawQuery = params.Encode()
return logoutUrl.String(), true
}
// RegistrationEnabled returns whether registration is enabled for this authenticator. // RegistrationEnabled returns whether registration is enabled for this authenticator.
func (o OidcAuthenticator) RegistrationEnabled() bool { func (o OidcAuthenticator) RegistrationEnabled() bool {
return o.registrationEnabled return o.registrationEnabled

View File

@@ -16,7 +16,6 @@ func parseOauthUserInfo(
) (*domain.AuthenticatorUserInfo, error) { ) (*domain.AuthenticatorUserInfo, error) {
var isAdmin bool var isAdmin bool
var adminInfoAvailable bool var adminInfoAvailable bool
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
// first try to match the is_admin field against the given regex // first try to match the is_admin field against the given regex
if mapping.IsAdmin != "" { if mapping.IsAdmin != "" {
@@ -30,6 +29,7 @@ func parseOauthUserInfo(
// next try to parse the user's groups // next try to parse the user's groups
if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" { if !isAdmin && mapping.UserGroups != "" && adminMapping.AdminGroupRegex != "" {
adminInfoAvailable = true adminInfoAvailable = true
userGroups := internal.MapDefaultStringSlice(raw, mapping.UserGroups, nil)
re := adminMapping.GetAdminGroupRegex() re := adminMapping.GetAdminGroupRegex()
for _, group := range userGroups { for _, group := range userGroups {
if re.MatchString(strings.TrimSpace(group)) { if re.MatchString(strings.TrimSpace(group)) {
@@ -42,7 +42,6 @@ func parseOauthUserInfo(
userInfo := &domain.AuthenticatorUserInfo{ userInfo := &domain.AuthenticatorUserInfo{
Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")), Identifier: domain.UserIdentifier(internal.MapDefaultString(raw, mapping.UserIdentifier, "")),
Email: internal.MapDefaultString(raw, mapping.Email, ""), Email: internal.MapDefaultString(raw, mapping.Email, ""),
UserGroups: userGroups,
Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""), Firstname: internal.MapDefaultString(raw, mapping.Firstname, ""),
Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""), Lastname: internal.MapDefaultString(raw, mapping.Lastname, ""),
Phone: internal.MapDefaultString(raw, mapping.Phone, ""), Phone: internal.MapDefaultString(raw, mapping.Phone, ""),

View File

@@ -96,7 +96,6 @@ func Test_parseOauthUserInfo_admin_group(t *testing.T) {
assert.Equal(t, info.Firstname, "Test User") assert.Equal(t, info.Firstname, "Test User")
assert.Equal(t, info.Lastname, "") assert.Equal(t, info.Lastname, "")
assert.Equal(t, info.Email, "test@mydomain.net") assert.Equal(t, info.Email, "test@mydomain.net")
assert.Equal(t, info.UserGroups, []string{"abuse@mydomain.net", "postmaster@mydomain.net", "wgportal-admins@mydomain.net"})
} }
func Test_parseOauthUserInfo_admin_value(t *testing.T) { func Test_parseOauthUserInfo_admin_value(t *testing.T) {

View File

@@ -533,7 +533,6 @@ func (m Manager) create(ctx context.Context, user *domain.User) (*domain.User, e
} }
err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) { err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
user.CopyCalculatedAttributes(u, false)
return user, nil return user, nil
}) })
if err != nil { if err != nil {

View File

@@ -36,7 +36,6 @@ type InterfaceAndPeerDatabaseRepo interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr) (map[domain.Cidr][]domain.Cidr, error)
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
GetAllUsers(ctx context.Context) ([]domain.User, error)
} }
type WgQuickController interface { type WgQuickController interface {
@@ -61,7 +60,6 @@ type Manager struct {
wg *ControllerManager wg *ControllerManager
userLockMap *sync.Map userLockMap *sync.Map
interfaceLockMap *sync.Map
} }
func NewWireGuardManager( func NewWireGuardManager(
@@ -76,7 +74,6 @@ func NewWireGuardManager(
wg: wg, wg: wg,
db: db, db: db,
userLockMap: &sync.Map{}, userLockMap: &sync.Map{},
interfaceLockMap: &sync.Map{},
} }
m.connectToMessageBus() m.connectToMessageBus()
@@ -96,11 +93,10 @@ func (m Manager) connectToMessageBus() {
_ = m.bus.Subscribe(app.TopicUserDisabled, m.handleUserDisabledEvent) _ = m.bus.Subscribe(app.TopicUserDisabled, m.handleUserDisabledEvent)
_ = m.bus.Subscribe(app.TopicUserEnabled, m.handleUserEnabledEvent) _ = m.bus.Subscribe(app.TopicUserEnabled, m.handleUserEnabledEvent)
_ = m.bus.Subscribe(app.TopicUserDeleted, m.handleUserDeletedEvent) _ = m.bus.Subscribe(app.TopicUserDeleted, m.handleUserDeletedEvent)
_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreatedEvent)
} }
func (m Manager) handleUserCreationEvent(user domain.User) { func (m Manager) handleUserCreationEvent(user domain.User) {
if !m.cfg.Core.CreateDefaultPeerOnUserCreation { if !m.cfg.Core.CreateDefaultPeerOnCreation {
return return
} }
@@ -121,7 +117,7 @@ func (m Manager) handleUserCreationEvent(user domain.User) {
} }
func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) { func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
if !m.cfg.Core.CreateDefaultPeerOnLogin { if !m.cfg.Core.CreateDefaultPeer {
return return
} }
@@ -273,31 +269,6 @@ func (m Manager) handleUserDeletedEvent(user domain.User) {
} }
} }
// handleInterfaceCreatedEvent creates default peers for all existing users when a new interface is created.
// This ensures users that already exist (e.g. imported via a prior LDAP sync that had no interface available)
// also receive a default peer for the newly created interface.
func (m Manager) handleInterfaceCreatedEvent(iface domain.Interface) {
if !m.cfg.Core.CreateDefaultPeerOnUserCreation {
return
}
_, loaded := m.interfaceLockMap.LoadOrStore(iface.Identifier, "create")
if loaded {
return // another goroutine is already handling this interface
}
defer m.interfaceLockMap.Delete(iface.Identifier)
slog.Debug("handling new interface event", "interface", iface.Identifier)
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
err := m.CreateDefaultPeers(ctx, iface.Identifier)
if err != nil {
slog.Error("failed to create default peers on new interface",
"interface", iface.Identifier, "error", err)
}
}
func (m Manager) runExpiredPeersCheck(ctx context.Context) { func (m Manager) runExpiredPeersCheck(ctx context.Context) {
ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo()) ctx = domain.SetUserInfo(ctx, domain.SystemAdminContextUserInfo())

View File

@@ -387,7 +387,7 @@ func (m Manager) PrepareInterface(ctx context.Context) (*domain.Interface, error
SaveConfig: m.cfg.Advanced.ConfigStoragePath != "", SaveConfig: m.cfg.Advanced.ConfigStoragePath != "",
DisplayName: string(id), DisplayName: string(id),
Type: domain.InterfaceTypeServer, Type: domain.InterfaceTypeServer,
CreateDefaultPeer: m.cfg.DefaultPeerCreationEnabled(), CreateDefaultPeer: m.cfg.Core.CreateDefaultPeer,
DriverType: "", DriverType: "",
Disabled: nil, Disabled: nil,
DisabledReason: "", DisabledReason: "",

View File

@@ -94,6 +94,13 @@ func TestImportPeer_AddressMapping(t *testing.T) {
} }
} }
func (f *mockDB) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
return &domain.User{
Identifier: id,
IsAdmin: false,
}, nil
}
func TestInterface_IsUserAllowed(t *testing.T) { func TestInterface_IsUserAllowed(t *testing.T) {
cfg := &config.Config{ cfg := &config.Config{
Auth: config.Auth{ Auth: config.Auth{

View File

@@ -15,10 +15,6 @@ import (
// CreateDefaultPeer creates a default peer for the given user on all server interfaces. // CreateDefaultPeer creates a default peer for the given user on all server interfaces.
func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdentifier) error { func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdentifier) error {
if !m.cfg.DefaultPeerCreationEnabled() {
return nil
}
if err := domain.ValidateAdminAccessRights(ctx); err != nil { if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err return err
} }
@@ -28,21 +24,39 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
return fmt.Errorf("failed to fetch all interfaces: %w", err) return fmt.Errorf("failed to fetch all interfaces: %w", err)
} }
user, err := m.db.GetUser(ctx, userId) userPeers, err := m.db.GetUserPeers(context.Background(), userId)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch user: %w", err) return fmt.Errorf("failed to retrieve existing peers prior to default peer creation: %w", err)
} }
var newPeers []domain.Peer var newPeers []domain.Peer
for _, iface := range existingInterfaces { for _, iface := range existingInterfaces {
peer, err := m.prepareDefaultPeer(ctx, &iface, user) if iface.Type != domain.InterfaceTypeServer {
if err != nil { continue // only create default peers for server interfaces
return fmt.Errorf("failed to prepare default peer: %w", err)
} }
if peer != nil { if !iface.CreateDefaultPeer {
newPeers = append(newPeers, *peer) continue // only create default peers if the interface flag is set
} }
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
return peer.InterfaceIdentifier == iface.Identifier
})
if peerAlreadyCreated {
continue // skip creation if a peer already exists for this interface
}
peer, err := m.PreparePeer(ctx, iface.Identifier)
if err != nil {
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
}
peer.UserIdentifier = userId
peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
peer.AutomaticallyCreated = true
peer.GenerateDisplayName("Default")
newPeers = append(newPeers, *peer)
} }
for i, peer := range newPeers { for i, peer := range newPeers {
@@ -53,61 +67,9 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
} }
} }
slog.InfoContext(ctx, "created default peers for user", "user", userId, "count", len(newPeers)) slog.InfoContext(ctx, "created default peers for user",
"user", userId,
return nil "count", len(newPeers))
}
// CreateDefaultPeers creates default peers for all existing users on the given interface.
func (m Manager) CreateDefaultPeers(ctx context.Context, interfaceId domain.InterfaceIdentifier) error {
if !m.cfg.DefaultPeerCreationEnabled() {
return nil
}
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
iface, err := m.db.GetInterface(ctx, interfaceId)
if err != nil {
return fmt.Errorf("failed to fetch interface %s: %w", interfaceId, err)
}
if !iface.CreateDefaultPeers() {
return nil
}
users, err := m.db.GetAllUsers(ctx)
if err != nil {
return fmt.Errorf("failed to fetch all users: %w", err)
}
var errs error
var peerCount int
for _, user := range users {
peer, err := m.prepareDefaultPeer(ctx, iface, &user)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to prepare default peer for user %s: %w",
user.Identifier, err))
continue
}
if peer == nil {
continue
}
_, err = m.CreatePeer(ctx, peer)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to create default peer for user %s: %w",
user.Identifier, err))
continue
}
peerCount++
}
if errs != nil {
return fmt.Errorf("failed to create default peers for interface %s: %w", interfaceId, errs)
}
slog.InfoContext(ctx, "created default peers for interface", "interface", interfaceId, "count", peerCount)
return nil return nil
} }
@@ -677,39 +639,4 @@ func (m Manager) checkInterfaceAccess(ctx context.Context, id domain.InterfaceId
return nil return nil
} }
func (m Manager) prepareDefaultPeer(ctx context.Context, iface *domain.Interface, user *domain.User) (
*domain.Peer,
error,
) {
if !iface.CreateDefaultPeers() || !user.CreateDefaultPeers() {
return nil, nil
}
userPeers, err := m.db.GetUserPeers(ctx, user.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to retrieve existing peers prior to default peer creation: %w", err)
}
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
// Ignore the AutomaticallyCreated flag on the peer.
// If a user already has a peer for a given interface, no default peer should be created.
return peer.InterfaceIdentifier == iface.Identifier
})
if peerAlreadyCreated {
return nil, nil // skip creation if a peer already exists for this interface
}
peer, err := m.PreparePeer(ctx, iface.Identifier)
if err != nil {
return nil, fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
}
peer.UserIdentifier = user.Identifier
peer.Notes = fmt.Sprintf("Default peer created for user %s", user.Identifier)
peer.AutomaticallyCreated = true
peer.GenerateDisplayName("Default")
return peer, nil
}
// endregion helper-functions // endregion helper-functions

View File

@@ -61,7 +61,6 @@ type mockDB struct {
savedPeers map[domain.PeerIdentifier]*domain.Peer savedPeers map[domain.PeerIdentifier]*domain.Peer
iface *domain.Interface iface *domain.Interface
interfaces []domain.Interface interfaces []domain.Interface
users []domain.User
} }
func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) { func (f *mockDB) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error) {
@@ -142,15 +141,6 @@ func (f *mockDB) GetUsedIpsPerSubnet(ctx context.Context, subnets []domain.Cidr)
) { ) {
return map[domain.Cidr][]domain.Cidr{}, nil return map[domain.Cidr][]domain.Cidr{}, nil
} }
func (f *mockDB) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
return &domain.User{
Identifier: id,
IsAdmin: false,
}, nil
}
func (f *mockDB) GetAllUsers(ctx context.Context) ([]domain.User, error) {
return f.users, nil
}
// --- Test --- // --- Test ---
@@ -215,7 +205,7 @@ func TestCreatePeer_SetsIdentifier_FromPublicKey(t *testing.T) {
func TestCreateDefaultPeer_RespectsInterfaceFlag(t *testing.T) { func TestCreateDefaultPeer_RespectsInterfaceFlag(t *testing.T) {
// Arrange // Arrange
cfg := &config.Config{} cfg := &config.Config{}
cfg.Core.CreateDefaultPeerOnLogin = true cfg.Core.CreateDefaultPeer = true
bus := &mockBus{} bus := &mockBus{}
ctrlMgr := &ControllerManager{ ctrlMgr := &ControllerManager{

View File

@@ -258,10 +258,6 @@ type OpenIDConnectProvider struct {
// AllowedDomains defines the list of allowed domains // AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"` AllowedDomains []string `yaml:"allowed_domains"`
// AllowedUserGroups defines the list of allowed user groups.
// If not empty, at least one group from the user's group claim must match.
AllowedUserGroups []string `yaml:"allowed_user_groups"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields // FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"` FieldMap OauthFields `yaml:"field_map"`
@@ -278,11 +274,6 @@ type OpenIDConnectProvider struct {
// If LogSensitiveInfo is set to true, sensitive information retrieved from the OIDC provider will be logged in trace level. // If LogSensitiveInfo is set to true, sensitive information retrieved from the OIDC provider will be logged in trace level.
// This also includes OAuth tokens! Keep this disabled in production! // This also includes OAuth tokens! Keep this disabled in production!
LogSensitiveInfo bool `yaml:"log_sensitive_info"` LogSensitiveInfo bool `yaml:"log_sensitive_info"`
// LogoutIdpSession controls whether the user's session at the OIDC provider is terminated on logout.
// If set to true (default), the user will be redirected to the IdP's end_session_endpoint after local logout.
// If set to false, only the local wg-portal session is invalidated.
LogoutIdpSession *bool `yaml:"logout_idp_session"`
} }
// OAuthProvider contains the configuration for the OAuth provider. // OAuthProvider contains the configuration for the OAuth provider.
@@ -312,10 +303,6 @@ type OAuthProvider struct {
// AllowedDomains defines the list of allowed domains // AllowedDomains defines the list of allowed domains
AllowedDomains []string `yaml:"allowed_domains"` AllowedDomains []string `yaml:"allowed_domains"`
// AllowedUserGroups defines the list of allowed user groups.
// If not empty, at least one group from the user's group claim must match.
AllowedUserGroups []string `yaml:"allowed_user_groups"`
// FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields // FieldMap is used to map the names of the user-info endpoint fields to wg-portal fields
FieldMap OauthFields `yaml:"field_map"` FieldMap OauthFields `yaml:"field_map"`

View File

@@ -22,11 +22,8 @@ type Config struct {
AdminApiToken string `yaml:"admin_api_token"` // if set, the API access is enabled automatically AdminApiToken string `yaml:"admin_api_token"` // if set, the API access is enabled automatically
EditableKeys bool `yaml:"editable_keys"` EditableKeys bool `yaml:"editable_keys"`
CreateDefaultPeer bool `yaml:"create_default_peer"` // DEPRECATED: in favor of CreateDefaultPeerOnLogin CreateDefaultPeer bool `yaml:"create_default_peer"`
CreateDefaultPeerOnCreation bool `yaml:"create_default_peer_on_creation"` // DEPRECATED: in favor of CreateDefaultPeerOnUserCreation CreateDefaultPeerOnCreation bool `yaml:"create_default_peer_on_creation"`
CreateDefaultPeerOnLogin bool `yaml:"create_default_peer_on_login"`
CreateDefaultPeerOnUserCreation bool `yaml:"create_default_peer_on_user_creation"`
CreateDefaultPeerOnInterfaceCreation bool `yaml:"create_default_peer_on_interface_creation"`
ReEnablePeerAfterUserEnable bool `yaml:"re_enable_peer_after_user_enable"` ReEnablePeerAfterUserEnable bool `yaml:"re_enable_peer_after_user_enable"`
DeletePeerAfterUserDeleted bool `yaml:"delete_peer_after_user_deleted"` DeletePeerAfterUserDeleted bool `yaml:"delete_peer_after_user_deleted"`
SelfProvisioningAllowed bool `yaml:"self_provisioning_allowed"` SelfProvisioningAllowed bool `yaml:"self_provisioning_allowed"`
@@ -81,7 +78,7 @@ func (c *Config) LogStartupValues() {
slog.Debug("Config Features", slog.Debug("Config Features",
"editableKeys", c.Core.EditableKeys, "editableKeys", c.Core.EditableKeys,
"createDefaultPeerOnCreation", c.Core.CreateDefaultPeerOnUserCreation, "createDefaultPeerOnCreation", c.Core.CreateDefaultPeerOnCreation,
"reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable, "reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable,
"deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted, "deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted,
"selfProvisioningAllowed", c.Core.SelfProvisioningAllowed, "selfProvisioningAllowed", c.Core.SelfProvisioningAllowed,
@@ -115,13 +112,6 @@ func (c *Config) LogStartupValues() {
} }
// DefaultPeerCreationEnabled returns true if at least one default peer generation mechanism is enabled.
func (c *Config) DefaultPeerCreationEnabled() bool {
return c.Core.CreateDefaultPeerOnLogin ||
c.Core.CreateDefaultPeerOnInterfaceCreation ||
c.Core.CreateDefaultPeerOnUserCreation
}
// defaultConfig returns the default configuration // defaultConfig returns the default configuration
func defaultConfig() *Config { func defaultConfig() *Config {
cfg := &Config{} cfg := &Config{}
@@ -132,13 +122,8 @@ func defaultConfig() *Config {
cfg.Core.AdminApiToken = getEnvStr("WG_PORTAL_CORE_ADMIN_API_TOKEN", "") // by default, the API access is disabled cfg.Core.AdminApiToken = getEnvStr("WG_PORTAL_CORE_ADMIN_API_TOKEN", "") // by default, the API access is disabled
cfg.Core.ImportExisting = getEnvBool("WG_PORTAL_CORE_IMPORT_EXISTING", true) cfg.Core.ImportExisting = getEnvBool("WG_PORTAL_CORE_IMPORT_EXISTING", true)
cfg.Core.RestoreState = getEnvBool("WG_PORTAL_CORE_RESTORE_STATE", true) cfg.Core.RestoreState = getEnvBool("WG_PORTAL_CORE_RESTORE_STATE", true)
cfg.Core.CreateDefaultPeer = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER", false) // deprecated cfg.Core.CreateDefaultPeer = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER", false)
cfg.Core.CreateDefaultPeerOnCreation = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION", cfg.Core.CreateDefaultPeerOnCreation = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION", false)
false) // deprecated
cfg.Core.CreateDefaultPeerOnLogin = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER", false)
cfg.Core.CreateDefaultPeerOnUserCreation = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_USER_CREATION", false)
cfg.Core.CreateDefaultPeerOnInterfaceCreation = getEnvBool("WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_INTERFACE_CREATION",
false)
cfg.Core.EditableKeys = getEnvBool("WG_PORTAL_CORE_EDITABLE_KEYS", true) cfg.Core.EditableKeys = getEnvBool("WG_PORTAL_CORE_EDITABLE_KEYS", true)
cfg.Core.SelfProvisioningAllowed = getEnvBool("WG_PORTAL_CORE_SELF_PROVISIONING_ALLOWED", false) cfg.Core.SelfProvisioningAllowed = getEnvBool("WG_PORTAL_CORE_SELF_PROVISIONING_ALLOWED", false)
cfg.Core.ReEnablePeerAfterUserEnable = getEnvBool("WG_PORTAL_CORE_RE_ENABLE_PEER_AFTER_USER_ENABLE", true) cfg.Core.ReEnablePeerAfterUserEnable = getEnvBool("WG_PORTAL_CORE_RE_ENABLE_PEER_AFTER_USER_ENABLE", true)
@@ -261,8 +246,6 @@ func GetConfig() (*Config, error) {
} }
} }
handleDeprecatedConfigValues(cfg)
return cfg, nil return cfg, nil
} }
@@ -356,18 +339,3 @@ func getEnvDuration(name string, fallback time.Duration) time.Duration {
return d return d
} }
func handleDeprecatedConfigValues(cfg *Config) {
// deprecated, will be removed in 2.4
if cfg.Core.CreateDefaultPeer {
slog.Warn("DEPRECATION WARNING: deprecated core config option: create_default_peer (WG_PORTAL_CORE_CREATE_DEFAULT_PEER)")
cfg.Core.CreateDefaultPeerOnLogin = true
}
// deprecated, will be removed in 2.4
if cfg.Core.CreateDefaultPeerOnCreation {
slog.Warn("DEPRECATION WARNING: deprecated core config option: create_default_peer_on_creation (WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION)")
cfg.Core.CreateDefaultPeerOnUserCreation = true
cfg.Core.CreateDefaultPeerOnInterfaceCreation = true
}
}

View File

@@ -12,7 +12,6 @@ type LoginProviderInfo struct {
type AuthenticatorUserInfo struct { type AuthenticatorUserInfo struct {
Identifier UserIdentifier Identifier UserIdentifier
Email string Email string
UserGroups []string
Firstname string Firstname string
Lastname string Lastname string
Phone string Phone string

View File

@@ -240,18 +240,6 @@ func (i *Interface) GetRoutingTable() int {
} }
} }
// CreateDefaultPeers determines whether default peers should be created for this interface.
func (i *Interface) CreateDefaultPeers() bool {
if !i.CreateDefaultPeer {
return false // only create default peers if the interface flag is set
}
if i.Type != InterfaceTypeServer {
return false // only create default peers for server interfaces
}
return true
}
type PhysicalInterface struct { type PhysicalInterface struct {
Identifier InterfaceIdentifier // device name, for example: wg0 Identifier InterfaceIdentifier // device name, for example: wg0
KeyPair // private/public Key of the server interface KeyPair // private/public Key of the server interface

View File

@@ -139,29 +139,3 @@ func TestInterface_GetRoutingTableNonLocal(t *testing.T) {
iface.RoutingTable = "abc" iface.RoutingTable = "abc"
assert.Equal(t, 0, iface.GetRoutingTable()) assert.Equal(t, 0, iface.GetRoutingTable())
} }
func TestInterface_CreateDefaultPeers(t *testing.T) {
iface := &Interface{}
assert.False(t, iface.CreateDefaultPeers())
iface.CreateDefaultPeer = true
assert.False(t, iface.CreateDefaultPeers()) // still wrong type
iface2 := &Interface{Type: InterfaceTypeServer}
assert.False(t, iface2.CreateDefaultPeers()) // CreateDefaultPeer flag is false
iface2.CreateDefaultPeer = true
assert.True(t, iface2.CreateDefaultPeers())
iface3 := &Interface{Type: InterfaceTypeClient}
assert.False(t, iface3.CreateDefaultPeers())
iface3.CreateDefaultPeer = true
assert.False(t, iface3.CreateDefaultPeers())
iface4 := &Interface{Type: InterfaceTypeAny}
assert.False(t, iface4.CreateDefaultPeers())
iface4.CreateDefaultPeer = true
assert.False(t, iface4.CreateDefaultPeers())
}

View File

@@ -68,7 +68,7 @@ type User struct {
WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication WebAuthnCredentialList []UserWebauthnCredential `gorm:"foreignKey:user_identifier"` // the webauthn credentials of the user, used for webauthn authentication
// API token for REST API access // API token for REST API access
ApiToken string `form:"api_token" binding:"omitempty" gorm:"serializer:encstr"` ApiToken string `form:"api_token" binding:"omitempty"`
ApiTokenCreated *time.Time ApiTokenCreated *time.Time
LinkedPeerCount int `gorm:"-"` LinkedPeerCount int `gorm:"-"`
@@ -270,18 +270,6 @@ func (u *User) DisplayName() string {
return displayName return displayName
} }
// CreateDefaultPeers determines whether default peers should be created for this user.
func (u *User) CreateDefaultPeers() bool {
if u.IsDisabled() {
return false
}
if u.IsLocked() {
return false
}
return true
}
// region webauthn // region webauthn
func (u *User) WebAuthnID() []byte { func (u *User) WebAuthnID() []byte {

View File

@@ -145,17 +145,3 @@ func TestUser_HashPassword(t *testing.T) {
user.Password = "" user.Password = ""
assert.NoError(t, user.HashPassword()) assert.NoError(t, user.HashPassword())
} }
func TestUser_CreateDefaultPeers(t *testing.T) {
user := &User{}
assert.True(t, user.CreateDefaultPeers())
user2 := &User{Disabled: &time.Time{}}
assert.False(t, user2.CreateDefaultPeers())
user3 := &User{Locked: &time.Time{}}
assert.False(t, user3.CreateDefaultPeers())
user4 := &User{Disabled: &time.Time{}, Locked: &time.Time{}}
assert.False(t, user4.CreateDefaultPeers())
}