mirror of
https://github.com/h44z/wg-portal.git
synced 2026-04-14 19:46:24 +00:00
Compare commits
6 Commits
v2.2.3
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa74f17f06 | ||
|
|
b44b79d42c | ||
|
|
71806455dd | ||
|
|
9b437205b1 | ||
|
|
401642701a | ||
|
|
72f9123592 |
4
.github/workflows/chart.yml
vendored
4
.github/workflows/chart.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
- name: Run chart-testing (lint)
|
- name: Run chart-testing (lint)
|
||||||
run: ct lint --config ct.yaml
|
run: ct lint --config ct.yaml
|
||||||
|
|
||||||
- uses: nolar/setup-k3d-k3s@8bf8d22160e8b1d184dcb780e390d6952a7eec65 # v1.0.10
|
- uses: nolar/setup-k3d-k3s@62c9d1bd2bc843275c85d2e7dcd696edc1160eee # v1.1.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
12
.github/workflows/docker-publish.yml
vendored
12
.github/workflows/docker-publish.yml
vendored
@@ -32,14 +32,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
type=semver,pattern=v{{major}}
|
type=semver,pattern=v{{major}}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
BUILD_VERSION=${{ env.BUILD_VERSION }}
|
||||||
|
|
||||||
- name: Export binaries from images
|
- name: Export binaries from images
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
done
|
done
|
||||||
|
|
||||||
- name: Upload binaries
|
- name: Upload binaries
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: binaries
|
name: binaries
|
||||||
path: binaries/wg-portal_linux*
|
path: binaries/wg-portal_linux*
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
name: binaries
|
name: binaries
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
with:
|
with:
|
||||||
files: 'wg-portal_linux*'
|
files: 'wg-portal_linux*'
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -1,7 +1,8 @@
|
|||||||
# 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
|
||||||
@@ -51,6 +52,11 @@ 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
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ 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
|
||||||
@@ -57,6 +58,7 @@ 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
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ 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
|
||||||
@@ -201,6 +204,9 @@ 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
|
||||||
|
|||||||
@@ -561,6 +561,10 @@ 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.
|
||||||
@@ -596,6 +600,10 @@ 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
|
||||||
@@ -639,6 +647,10 @@ 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.
|
||||||
|
|||||||
@@ -66,6 +66,40 @@ 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.
|
||||||
|
|||||||
121
frontend/src/components/Pagination.vue
Normal file
121
frontend/src/components/Pagination.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<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()">«</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()">»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,7 +11,6 @@ export const auditStore = defineStore('audit', {
|
|||||||
filter: "",
|
filter: "",
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
pageOffset: 0,
|
pageOffset: 0,
|
||||||
pages: [],
|
|
||||||
fetching: false,
|
fetching: false,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
@@ -41,33 +40,22 @@ 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() {
|
||||||
this.pageOffset += this.pageSize
|
if (this.hasNextPage) {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
this.calculatePages()
|
}
|
||||||
},
|
},
|
||||||
previousPage() {
|
previousPage() {
|
||||||
this.pageOffset -= this.pageSize
|
if (this.hasPrevPage) {
|
||||||
|
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() {
|
||||||
|
|||||||
@@ -108,12 +108,19 @@ 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 {
|
||||||
await apiWrapper.post(`/auth/logout`)
|
logoutResponse = 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!",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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
|
||||||
@@ -87,33 +86,22 @@ 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() {
|
||||||
this.pageOffset += this.pageSize
|
if (this.hasNextPage) {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
this.calculatePages()
|
}
|
||||||
},
|
},
|
||||||
previousPage() {
|
previousPage() {
|
||||||
this.pageOffset -= this.pageSize
|
if (this.hasPrevPage) {
|
||||||
|
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 = {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ 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
|
||||||
@@ -80,29 +79,19 @@ 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() {
|
||||||
this.pageOffset += this.pageSize
|
if (this.hasNextPage) {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
this.calculatePages()
|
}
|
||||||
},
|
},
|
||||||
previousPage() {
|
previousPage() {
|
||||||
this.pageOffset -= this.pageSize
|
if (this.hasPrevPage) {
|
||||||
|
this.pageOffset -= this.pageSize
|
||||||
this.calculatePages()
|
}
|
||||||
},
|
},
|
||||||
setPeers(peers) {
|
setPeers(peers) {
|
||||||
this.peers = peers
|
this.peers = peers
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const userStore = defineStore('users', {
|
|||||||
filter: "",
|
filter: "",
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
pageOffset: 0,
|
pageOffset: 0,
|
||||||
pages: [],
|
|
||||||
fetching: false,
|
fetching: false,
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
@@ -43,33 +42,22 @@ 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() {
|
||||||
this.pageOffset += this.pageSize
|
if (this.hasNextPage) {
|
||||||
|
this.pageOffset += this.pageSize
|
||||||
this.calculatePages()
|
}
|
||||||
},
|
},
|
||||||
previousPage() {
|
previousPage() {
|
||||||
this.pageOffset -= this.pageSize
|
if (this.hasPrevPage) {
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<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()
|
||||||
|
|
||||||
@@ -60,28 +61,24 @@ onMounted(async () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="mt-3">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<ul class="pagination pagination-sm">
|
<Pagination
|
||||||
<li :class="{disabled:audit.pageOffset===0}" class="page-item">
|
:currentPage="audit.currentPage"
|
||||||
<a class="page-link" @click="audit.previousPage">«</a>
|
:totalCount="audit.FilteredCount"
|
||||||
</li>
|
:pageSize="audit.pageSize"
|
||||||
|
:hasNextPage="audit.hasNextPage"
|
||||||
<li v-for="page in audit.pages" :key="page" :class="{active:audit.currentPage===page}" class="page-item">
|
:hasPrevPage="audit.hasPrevPage"
|
||||||
<a class="page-link" @click="audit.gotoPage(page)">{{page}}</a>
|
:onGotoPage="audit.gotoPage"
|
||||||
</li>
|
:onNextPage="audit.nextPage"
|
||||||
|
:onPrevPage="audit.previousPage"
|
||||||
<li :class="{disabled:!audit.hasNextPage}" class="page-item">
|
/>
|
||||||
<a class="page-link" @click="audit.nextPage">»</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-md-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" @click="audit.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="audit.pageSize" class="form-select" @change="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>
|
||||||
@@ -92,5 +89,4 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<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";
|
||||||
@@ -482,26 +483,23 @@ 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-6">
|
<div class="col-12 col-md-6">
|
||||||
<ul class="pagination pagination-sm">
|
<Pagination
|
||||||
<li :class="{disabled:peers.pageOffset===0}" class="page-item">
|
:currentPage="peers.currentPage"
|
||||||
<a class="page-link" @click="peers.previousPage">«</a>
|
:totalCount="peers.FilteredCount"
|
||||||
</li>
|
:pageSize="peers.pageSize"
|
||||||
|
:hasNextPage="peers.hasNextPage"
|
||||||
<li v-for="page in peers.pages" :key="page" :class="{active:peers.currentPage===page}" class="page-item">
|
:hasPrevPage="peers.hasPrevPage"
|
||||||
<a class="page-link" @click="peers.gotoPage(page)">{{page}}</a>
|
:onGotoPage="peers.gotoPage"
|
||||||
</li>
|
:onNextPage="peers.nextPage"
|
||||||
|
:onPrevPage="peers.previousPage"
|
||||||
<li :class="{disabled:!peers.hasNextPage}" class="page-item">
|
/>
|
||||||
<a class="page-link" @click="peers.nextPage">»</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-md-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" @click="peers.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="peers.pageSize" class="form-select" @change="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>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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";
|
||||||
|
|
||||||
@@ -66,7 +67,6 @@ 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,36 +185,33 @@ onMounted(async () => {
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<ul class="pagination pagination-sm">
|
<Pagination
|
||||||
<li :class="{ disabled: profile.pageOffset === 0 }" class="page-item">
|
:currentPage="profile.currentPage"
|
||||||
<a class="page-link" @click="profile.previousPage">«</a>
|
:totalCount="profile.FilteredPeerCount"
|
||||||
</li>
|
:pageSize="profile.pageSize"
|
||||||
|
:hasNextPage="profile.hasNextPage"
|
||||||
<li v-for="page in profile.pages" :key="page" :class="{ active: profile.currentPage === page }" class="page-item">
|
:hasPrevPage="profile.hasPrevPage"
|
||||||
<a class="page-link" @click="profile.gotoPage(page)">{{ page }}</a>
|
:onGotoPage="profile.gotoPage"
|
||||||
</li>
|
:onNextPage="profile.nextPage"
|
||||||
|
:onPrevPage="profile.previousPage"
|
||||||
<li :class="{ disabled: !profile.hasNextPage }" class="page-item">
|
/>
|
||||||
<a class="page-link" @click="profile.nextPage">»</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">
|
<label class="col-sm-6 col-form-label text-md-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" @click="profile.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="profile.pageSize" class="form-select" @change="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>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
<option value="999999999">{{ $t('general.pagination.all') }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div></template>
|
</div></template>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<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()
|
||||||
@@ -165,28 +166,24 @@ onMounted(() => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="mt-3">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<ul class="pagination pagination-sm">
|
<Pagination
|
||||||
<li :class="{disabled:users.pageOffset===0}" class="page-item">
|
:currentPage="users.currentPage"
|
||||||
<a class="page-link" @click="users.previousPage">«</a>
|
:totalCount="users.FilteredCount"
|
||||||
</li>
|
:pageSize="users.pageSize"
|
||||||
|
:hasNextPage="users.hasNextPage"
|
||||||
<li v-for="page in users.pages" :key="page" :class="{active:users.currentPage===page}" class="page-item">
|
:hasPrevPage="users.hasPrevPage"
|
||||||
<a class="page-link" @click="users.gotoPage(page)">{{page}}</a>
|
:onGotoPage="users.gotoPage"
|
||||||
</li>
|
:onNextPage="users.nextPage"
|
||||||
|
:onPrevPage="users.previousPage"
|
||||||
<li :class="{disabled:!users.hasNextPage}" class="page-item">
|
/>
|
||||||
<a class="page-link" @click="users.nextPage">»</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-12 col-md-6">
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-6 col-form-label text-end" for="paginationSelector">{{ $t('general.pagination.size') }}:</label>
|
<label class="col-sm-6 col-form-label text-md-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" @click="users.afterPageSizeChange()">
|
<select id="paginationSelector" v-model.number="users.pageSize" class="form-select" @change="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>
|
||||||
@@ -197,5 +194,4 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -9,8 +9,8 @@ require (
|
|||||||
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.1
|
github.com/go-playground/validator/v10 v10.30.2
|
||||||
github.com/go-webauthn/webauthn v0.16.1
|
github.com/go-webauthn/webauthn v0.16.3
|
||||||
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
|
||||||
@@ -41,7 +41,7 @@ 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.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.1 // 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
|
||||||
@@ -81,12 +81,14 @@ require (
|
|||||||
github.com/microsoft/go-mssqldb v1.9.6 // 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.19.2 // 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.3 // 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
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -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.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.1/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=
|
||||||
@@ -97,16 +97,16 @@ 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.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
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.1 h1:x5/SSki5/aIfogaRukqvbg/RXa3Sgxy/9vU7UfFPHKU=
|
github.com/go-webauthn/webauthn v0.16.3 h1:RorP0c6VbaKP0i0Jxf/vAf7EFb2lmdLW8GLKITeaN5A=
|
||||||
github.com/go-webauthn/webauthn v0.16.1/go.mod h1:RBS+rtQJMkE5VfMQ4diDA2VNrEL8OeUhp4Srz37FHbQ=
|
github.com/go-webauthn/webauthn v0.16.3/go.mod h1:R2xjJxSPat5PYKg5r6cUmqXgbHtbv4GmF6uGkqFMLNI=
|
||||||
github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk=
|
github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk=
|
||||||
github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4=
|
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=
|
||||||
@@ -195,6 +195,8 @@ 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=
|
||||||
@@ -234,6 +236,8 @@ 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.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
|
github.com/tinylib/msgp v1.6.3/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=
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ 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, error)
|
OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, 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 {
|
||||||
@@ -331,7 +333,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, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
user, idTokenHint, err := e.authService.OauthLoginStep2(loginCtx, provider, currentSession.OauthNonce,
|
||||||
oauthCode)
|
oauthCode)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -346,7 +348,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.setAuthenticatedUser(r, user)
|
e.setAuthenticatedUser(r, user, provider, idTokenHint)
|
||||||
|
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
queryParams := returnUrl.Query()
|
queryParams := returnUrl.Query()
|
||||||
@@ -359,7 +361,7 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User) {
|
func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User, oauthProvider, idTokenHint string) {
|
||||||
// start a fresh session
|
// start a fresh session
|
||||||
e.session.DestroyData(r.Context())
|
e.session.DestroyData(r.Context())
|
||||||
|
|
||||||
@@ -374,8 +376,9 @@ func (e AuthEndpoint) setAuthenticatedUser(r *http.Request, user *domain.User) {
|
|||||||
|
|
||||||
currentSession.OauthState = ""
|
currentSession.OauthState = ""
|
||||||
currentSession.OauthNonce = ""
|
currentSession.OauthNonce = ""
|
||||||
currentSession.OauthProvider = ""
|
currentSession.OauthProvider = oauthProvider
|
||||||
currentSession.OauthReturnTo = ""
|
currentSession.OauthReturnTo = ""
|
||||||
|
currentSession.OauthIdToken = idTokenHint
|
||||||
|
|
||||||
e.session.SetData(r.Context(), currentSession)
|
e.session.SetData(r.Context(), currentSession)
|
||||||
}
|
}
|
||||||
@@ -418,7 +421,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)
|
||||||
}
|
}
|
||||||
@@ -430,19 +433,33 @@ 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.Error
|
// @Success 200 {object} model.LogoutResponse
|
||||||
// @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.Error{Code: http.StatusOK, Message: "not logged in"})
|
respond.JSON(w, http.StatusOK, model.LogoutResponse{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.Error{Code: http.StatusOK, Message: "logout ok"})
|
respond.JSON(w, http.StatusOK, model.LogoutResponse{Message: "logout ok", RedirectUrl: redirectUrl})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,7 +710,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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ type SessionData struct {
|
|||||||
OauthNonce string
|
OauthNonce string
|
||||||
OauthProvider string
|
OauthProvider string
|
||||||
OauthReturnTo string
|
OauthReturnTo string
|
||||||
|
OauthIdToken string
|
||||||
|
|
||||||
WebAuthnData string
|
WebAuthnData string
|
||||||
|
|
||||||
@@ -89,5 +90,6 @@ func (s *SessionWrapper) defaultSessionData() SessionData {
|
|||||||
OauthNonce: "",
|
OauthNonce: "",
|
||||||
OauthProvider: "",
|
OauthProvider: "",
|
||||||
OauthReturnTo: "",
|
OauthReturnTo: "",
|
||||||
|
OauthIdToken: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,11 @@ 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.
|
||||||
@@ -497,31 +502,63 @@ 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, error) {
|
func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce, code string) (*domain.User, string, 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,
|
||||||
@@ -537,7 +574,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() {
|
||||||
@@ -549,7 +586,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)
|
||||||
@@ -561,7 +598,16 @@ func (a *Authenticator) OauthLoginStep2(ctx context.Context, providerId, nonce,
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return user, nil
|
return user, idTokenHint, 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(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type PlainOauthAuthenticator struct {
|
|||||||
userInfoLogging bool
|
userInfoLogging bool
|
||||||
sensitiveInfoLogging bool
|
sensitiveInfoLogging bool
|
||||||
allowedDomains []string
|
allowedDomains []string
|
||||||
|
allowedUserGroups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlainOauthAuthenticator(
|
func newPlainOauthAuthenticator(
|
||||||
@@ -60,6 +61,7 @@ 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
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,14 @@ 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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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"
|
||||||
@@ -26,6 +27,9 @@ 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(
|
||||||
@@ -61,6 +65,17 @@ 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
|
||||||
}
|
}
|
||||||
@@ -74,6 +89,38 @@ 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
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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 != "" {
|
||||||
@@ -29,7 +30,6 @@ 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,6 +42,7 @@ 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, ""),
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ 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) {
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ 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"`
|
||||||
|
|
||||||
@@ -274,6 +278,11 @@ 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.
|
||||||
@@ -303,6 +312,10 @@ 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"`
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user