mirror of
https://github.com/h44z/wg-portal.git
synced 2025-10-05 16:06:17 +00:00
Compare commits
24 Commits
passkey_su
...
v2.0.4
Author | SHA1 | Date | |
---|---|---|---|
|
b546eec4ed | ||
|
9be2133220 | ||
|
b05837b2d9 | ||
|
08c8f8eac0 | ||
|
d864e24145 | ||
|
5b56e58fe9 | ||
|
930ef7b573 | ||
|
8816165260 | ||
|
ab9995350f | ||
|
18296673d7 | ||
|
7df4e4b813 | ||
|
657c4307b3 | ||
|
b918fb6522 | ||
|
78deede360 | ||
|
a8fb4365cf | ||
|
0102588d23 | ||
|
4ccc59c109 | ||
|
e6b01a9903 | ||
|
2f79dd04c0 | ||
|
e5ed9736b3 | ||
|
c8353b85ae | ||
|
6142031387 | ||
|
dd86d0ff49 | ||
|
bdd426a679 |
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -66,10 +66,6 @@ jobs:
|
|||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=semver,pattern=v{{major}}.{{minor}}
|
type=semver,pattern=v{{major}}.{{minor}}
|
||||||
type=semver,pattern=v{{major}}
|
type=semver,pattern=v{{major}}
|
||||||
# add v{{major}} tag, even for beta or release-canidate releases
|
|
||||||
type=match,pattern=(v\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
|
||||||
# add {{major}} tag, even for beta releases or release-canidate releases
|
|
||||||
type=match,pattern=v(\d),group=1,enable=${{ contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
|
7
.github/workflows/pages.yml
vendored
7
.github/workflows/pages.yml
vendored
@@ -27,6 +27,13 @@ jobs:
|
|||||||
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
run: pip install mike mkdocs-material[imaging] mkdocs-minify-plugin mkdocs-swagger-ui-tag
|
||||||
|
|
||||||
- name: Publish documentation
|
- name: Publish documentation
|
||||||
|
if: ${{ ! startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
run: mike deploy --push ${{ github.ref_name }}
|
||||||
|
env:
|
||||||
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
GIT_COMMITTER_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: Publish latest documentation
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
run: mike deploy --push --update-aliases ${{ github.ref_name }} latest
|
||||||
env:
|
env:
|
||||||
GIT_COMMITTER_NAME: "github-actions[bot]"
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
||||||
|
@@ -24,7 +24,7 @@
|
|||||||
<div id="toasts"></div>
|
<div id="toasts"></div>
|
||||||
|
|
||||||
<!-- main application -->
|
<!-- main application -->
|
||||||
<div id="app"></div>
|
<div id="app" class="d-flex flex-column flex-grow-1"></div>
|
||||||
|
|
||||||
<!-- vue teleport will add modals and dialogs here -->
|
<!-- vue teleport will add modals and dialogs here -->
|
||||||
<div id="modals"></div>
|
<div id="modals"></div>
|
||||||
|
@@ -61,6 +61,26 @@ const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
|||||||
const wgVersion = ref(WGPORTAL_VERSION);
|
const wgVersion = ref(WGPORTAL_VERSION);
|
||||||
const currentYear = ref(new Date().getFullYear())
|
const currentYear = ref(new Date().getFullYear())
|
||||||
|
|
||||||
|
const userDisplayName = computed(() => {
|
||||||
|
let displayName = "Unknown";
|
||||||
|
if (auth.IsAuthenticated) {
|
||||||
|
if (auth.User.Firstname === "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Identifier;
|
||||||
|
} else if (auth.User.Firstname === "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Lastname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname === "") {
|
||||||
|
displayName = auth.User.Firstname;
|
||||||
|
} else if (auth.User.Firstname !== "" && auth.User.Lastname !== "") {
|
||||||
|
displayName = auth.User.Firstname + " " + auth.User.Lastname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad string to 20 characters so that the menu is always the same size on desktop
|
||||||
|
if (displayName.length < 20 && window.innerWidth > 992) {
|
||||||
|
displayName = displayName.padStart(20, "\u00A0");
|
||||||
|
}
|
||||||
|
return displayName;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -93,7 +113,7 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
<div class="navbar-nav d-flex justify-content-end">
|
<div class="navbar-nav d-flex justify-content-end">
|
||||||
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
<div v-if="auth.IsAuthenticated" class="nav-item dropdown">
|
||||||
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
<a aria-expanded="false" aria-haspopup="true" class="nav-link dropdown-toggle" data-bs-toggle="dropdown"
|
||||||
href="#" role="button">{{ auth.User.Firstname }} {{ auth.User.Lastname }}</a>
|
href="#" role="button">{{ userDisplayName }}</a>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
<RouterLink :to="{ name: 'profile' }" class="dropdown-item"><i class="fas fa-user"></i> {{ $t('menu.profile') }}</RouterLink>
|
||||||
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
<RouterLink :to="{ name: 'settings' }" class="dropdown-item" v-if="auth.IsAdmin || !settings.Setting('ApiAdminOnly')"><i class="fas fa-gears"></i> {{ $t('menu.settings') }}</RouterLink>
|
||||||
@@ -140,6 +160,7 @@ const currentYear = ref(new Date().getFullYear())
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer></template>
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@@ -32,7 +32,7 @@ const selectedInterface = computed(() => {
|
|||||||
function freshForm() {
|
function freshForm() {
|
||||||
return {
|
return {
|
||||||
Identifiers: [],
|
Identifiers: [],
|
||||||
Suffix: "",
|
Prefix: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ async function save() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
<label class="form-label mt-4">{{ $t('modals.peer-multi-create.prefix.label') }}</label>
|
||||||
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Suffix">
|
<input type="text" class="form-control" :placeholder="$t('modals.peer-multi-create.prefix.placeholder')" v-model="formData.Prefix">
|
||||||
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
<small class="form-text text-muted">{{ $t('modals.peer-multi-create.prefix.description') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
@@ -129,7 +129,7 @@ export const profileStore = defineStore('profile', {
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/enable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to activate API for ", currentUser, ": ", error)
|
console.log("Failed to activate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
@@ -143,7 +143,7 @@ export const profileStore = defineStore('profile', {
|
|||||||
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
return apiWrapper.post(`${baseUrl}/${base64_url_encode(currentUser)}/api/disable`)
|
||||||
.then(this.setUser)
|
.then(this.setUser)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.setPeers([])
|
this.fetching = false
|
||||||
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
console.log("Failed to deactivate API for ", currentUser, ": ", error)
|
||||||
notify({
|
notify({
|
||||||
title: "Backend Connection Failure",
|
title: "Backend Connection Failure",
|
||||||
|
10
go.mod
10
go.mod
@@ -16,11 +16,11 @@ require (
|
|||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
github.com/vardius/message-bus v1.1.5
|
github.com/vardius/message-bus v1.1.5
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.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.37.0
|
golang.org/x/crypto v0.38.0
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.33.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
@@ -28,7 +28,7 @@ require (
|
|||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
gorm.io/driver/sqlserver v1.5.4
|
gorm.io/driver/sqlserver v1.5.4
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.26.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -81,8 +81,8 @@ require (
|
|||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
golang.org/x/tools v0.32.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
21
go.sum
21
go.sum
@@ -198,9 +198,8 @@ github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 h1:q0hKh5a5FRkhuTb5
|
|||||||
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||||
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
github.com/vardius/message-bus v1.1.5 h1:YSAC2WB4HRlwc4neFPTmT88kzzoiQ+9WRRbej/E/LZc=
|
||||||
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
github.com/vardius/message-bus v1.1.5/go.mod h1:6xladCV2lMkUAE4bzzS85qKOiB5miV7aBVRafiTJGqw=
|
||||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
|
||||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
|
||||||
@@ -221,8 +220,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -249,8 +248,8 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -289,8 +288,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-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=
|
||||||
@@ -322,8 +321,8 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
|
|||||||
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
|
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
|
||||||
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA=
|
||||||
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc=
|
||||||
|
@@ -57,6 +57,52 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth/login/{provider}/callback": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Handle the OAuth callback.",
|
||||||
|
"operationId": "auth_handleOauthCallbackGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/login/{provider}/init": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Authentication"
|
||||||
|
],
|
||||||
|
"summary": "Initiate the OAuth login flow.",
|
||||||
|
"operationId": "auth_handleOauthInitiateGet",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.LoginProviderInfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/auth/logout": {
|
"/auth/logout": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1805,7 +1851,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Suffix": {
|
"Prefix": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -206,7 +206,7 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
Suffix:
|
Prefix:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
model.Peer:
|
model.Peer:
|
||||||
@@ -383,6 +383,8 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
MailLinkOnly:
|
MailLinkOnly:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
MinPasswordLength:
|
||||||
|
type: integer
|
||||||
PersistentConfigSupported:
|
PersistentConfigSupported:
|
||||||
type: boolean
|
type: boolean
|
||||||
SelfProvisioning:
|
SelfProvisioning:
|
||||||
@@ -456,7 +458,22 @@ paths:
|
|||||||
summary: Get all available audit entries. Ordered by timestamp.
|
summary: Get all available audit entries. Ordered by timestamp.
|
||||||
tags:
|
tags:
|
||||||
- Audit
|
- Audit
|
||||||
/auth/{provider}/callback:
|
/auth/login:
|
||||||
|
post:
|
||||||
|
operationId: auth_handleLoginPost
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.LoginProviderInfo'
|
||||||
|
type: array
|
||||||
|
summary: Get all available external login providers.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
/auth/login/{provider}/callback:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthCallbackGet
|
operationId: auth_handleOauthCallbackGet
|
||||||
produces:
|
produces:
|
||||||
@@ -471,7 +488,7 @@ paths:
|
|||||||
summary: Handle the OAuth callback.
|
summary: Handle the OAuth callback.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/{provider}/init:
|
/auth/login/{provider}/init:
|
||||||
get:
|
get:
|
||||||
operationId: auth_handleOauthInitiateGet
|
operationId: auth_handleOauthInitiateGet
|
||||||
produces:
|
produces:
|
||||||
@@ -486,21 +503,6 @@ paths:
|
|||||||
summary: Initiate the OAuth login flow.
|
summary: Initiate the OAuth login flow.
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/auth/login:
|
|
||||||
post:
|
|
||||||
operationId: auth_handleLoginPost
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/model.LoginProviderInfo'
|
|
||||||
type: array
|
|
||||||
summary: Get all available external login providers.
|
|
||||||
tags:
|
|
||||||
- Authentication
|
|
||||||
/auth/logout:
|
/auth/logout:
|
||||||
post:
|
post:
|
||||||
operationId: auth_handleLogoutPost
|
operationId: auth_handleLogoutPost
|
||||||
|
@@ -138,7 +138,8 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
|||||||
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
s.versions[version].HandleFunc("GET /swagger/index.html", s.rapiDocHandler(version)) // Deprecated: old link
|
||||||
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
|
s.versions[version].HandleFunc("GET /doc.html", s.rapiDocHandler(version))
|
||||||
|
|
||||||
groupSetupFn(s.versions[version])
|
versionGroup := s.versions[version].Group()
|
||||||
|
groupSetupFn(versionGroup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -132,7 +133,7 @@ func (e AuthEndpoint) handleSessionInfoGet() http.HandlerFunc {
|
|||||||
// @Summary Initiate the OAuth login flow.
|
// @Summary Initiate the OAuth login flow.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.LoginProviderInfo
|
// @Success 200 {object} []model.LoginProviderInfo
|
||||||
// @Router /auth/{provider}/init [get]
|
// @Router /auth/login/{provider}/init [get]
|
||||||
func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
func (e AuthEndpoint) handleOauthInitiateGet() 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())
|
||||||
@@ -177,6 +178,8 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
|
|
||||||
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
authCodeUrl, state, nonce, err := e.authService.OauthLoginStep1(context.Background(), provider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
slog.Debug("failed to create oauth auth code URL",
|
||||||
|
"provider", provider, "error", err)
|
||||||
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
if autoRedirect && e.isValidReturnUrl(returnTo) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@@ -211,7 +214,7 @@ func (e AuthEndpoint) handleOauthInitiateGet() http.HandlerFunc {
|
|||||||
// @Summary Handle the OAuth callback.
|
// @Summary Handle the OAuth callback.
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} []model.LoginProviderInfo
|
// @Success 200 {object} []model.LoginProviderInfo
|
||||||
// @Router /auth/{provider}/callback [get]
|
// @Router /auth/login/{provider}/callback [get]
|
||||||
func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
func (e AuthEndpoint) handleOauthCallbackGet() 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())
|
||||||
@@ -249,6 +252,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
oauthState := request.Query(r, "state")
|
oauthState := request.Query(r, "state")
|
||||||
|
|
||||||
if provider != currentSession.OauthProvider {
|
if provider != currentSession.OauthProvider {
|
||||||
|
slog.Debug("invalid oauth provider in callback",
|
||||||
|
"expected", currentSession.OauthProvider, "got", provider, "state", oauthState)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@@ -258,6 +263,8 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if oauthState != currentSession.OauthState {
|
if oauthState != currentSession.OauthState {
|
||||||
|
slog.Debug("invalid oauth state in callback",
|
||||||
|
"expected", currentSession.OauthState, "got", oauthState, "provider", provider)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
@@ -267,11 +274,13 @@ func (e AuthEndpoint) handleOauthCallbackGet() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loginCtx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
|
loginCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) // avoid long waits
|
||||||
user, 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 {
|
||||||
|
slog.Debug("failed to process oauth code",
|
||||||
|
"provider", provider, "state", oauthState, "error", err)
|
||||||
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
if returnUrl != nil && e.isValidReturnUrl(returnUrl.String()) {
|
||||||
redirectToReturn()
|
redirectToReturn()
|
||||||
} else {
|
} else {
|
||||||
|
@@ -172,13 +172,13 @@ func NewDomainPeer(src *Peer) *domain.Peer {
|
|||||||
|
|
||||||
type MultiPeerRequest struct {
|
type MultiPeerRequest struct {
|
||||||
Identifiers []string `json:"Identifiers"`
|
Identifiers []string `json:"Identifiers"`
|
||||||
Suffix string `json:"Suffix"`
|
Prefix string `json:"Prefix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
|
func NewDomainPeerCreationRequest(src *MultiPeerRequest) *domain.PeerCreationRequest {
|
||||||
return &domain.PeerCreationRequest{
|
return &domain.PeerCreationRequest{
|
||||||
UserIdentifiers: src.Identifiers,
|
UserIdentifiers: src.Identifiers,
|
||||||
Suffix: src.Suffix,
|
Prefix: src.Prefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,11 +47,18 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
|||||||
}
|
}
|
||||||
latestVersion := "1.0.9"
|
latestVersion := "1.0.9"
|
||||||
if lastVersion.Version != latestVersion {
|
if lastVersion.Version != latestVersion {
|
||||||
return fmt.Errorf("unsupported old version, update to database version %s first: %w", latestVersion, err)
|
return fmt.Errorf("unsupported old version, update to database version %s first", latestVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("found valid V1 database", "version", lastVersion.Version)
|
slog.Info("found valid V1 database", "version", lastVersion.Version)
|
||||||
|
|
||||||
|
// validate target database
|
||||||
|
if err := validateTargetDatabase(db); err != nil {
|
||||||
|
return fmt.Errorf("target database validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("found valid target database, starting migration...")
|
||||||
|
|
||||||
if err := migrateV1Users(oldDb, db); err != nil {
|
if err := migrateV1Users(oldDb, db); err != nil {
|
||||||
return fmt.Errorf("user migration failed: %w", err)
|
return fmt.Errorf("user migration failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -70,6 +77,36 @@ func migrateFromV1(db *gorm.DB, source, typ string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateTargetDatabase checks if the target database is empty and ready for migration.
|
||||||
|
func validateTargetDatabase(db *gorm.DB) error {
|
||||||
|
var count int64
|
||||||
|
err := db.Model(&domain.User{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check user table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d users, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&domain.Interface{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check interface table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d interfaces, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(&domain.Peer{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check peer table: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return fmt.Errorf("target database contains %d peers, please use an empty database for migration", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
||||||
type User struct {
|
type User struct {
|
||||||
Email string `gorm:"primaryKey"`
|
Email string `gorm:"primaryKey"`
|
||||||
@@ -123,7 +160,7 @@ func migrateV1Users(oldDb, newDb *gorm.DB) error {
|
|||||||
LinkedPeerCount: 0,
|
LinkedPeerCount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newUser).Error; err != nil {
|
if err := newDb.Create(&newUser).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
return fmt.Errorf("failed to migrate user %s: %w", oldUser.Email, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +254,8 @@ func migrateV1Interfaces(oldDb, newDb *gorm.DB) error {
|
|||||||
PeerDefPostDown: "",
|
PeerDefPostDown: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newInterface).Error; err != nil {
|
// Create new interface with associations
|
||||||
|
if err := newDb.Create(&newInterface).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
return fmt.Errorf("failed to migrate device %s: %w", oldDevice.DeviceName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +400,7 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := newDb.Save(&newPeer).Error; err != nil {
|
if err := newDb.Create(&newPeer).Error; err != nil {
|
||||||
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
return fmt.Errorf("failed to migrate peer %s (%s): %w", oldPeer.Identifier, oldPeer.PublicKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ package wireguard
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
@@ -76,6 +77,8 @@ type Manager struct {
|
|||||||
db InterfaceAndPeerDatabaseRepo
|
db InterfaceAndPeerDatabaseRepo
|
||||||
wg InterfaceController
|
wg InterfaceController
|
||||||
quick WgQuickController
|
quick WgQuickController
|
||||||
|
|
||||||
|
userLockMap *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWireGuardManager(
|
func NewWireGuardManager(
|
||||||
@@ -86,11 +89,12 @@ func NewWireGuardManager(
|
|||||||
db InterfaceAndPeerDatabaseRepo,
|
db InterfaceAndPeerDatabaseRepo,
|
||||||
) (*Manager, error) {
|
) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
wg: wg,
|
wg: wg,
|
||||||
db: db,
|
db: db,
|
||||||
quick: quick,
|
quick: quick,
|
||||||
|
userLockMap: &sync.Map{},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.connectToMessageBus()
|
m.connectToMessageBus()
|
||||||
@@ -117,6 +121,12 @@ func (m Manager) handleUserCreationEvent(user domain.User) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, loaded := m.userLockMap.LoadOrStore(user.Identifier, "create")
|
||||||
|
if loaded {
|
||||||
|
return // another goroutine is already handling this user
|
||||||
|
}
|
||||||
|
defer m.userLockMap.Delete(user.Identifier)
|
||||||
|
|
||||||
slog.Debug("handling new user event", "user", user.Identifier)
|
slog.Debug("handling new user event", "user", user.Identifier)
|
||||||
|
|
||||||
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
|
ctx := domain.SetUserInfo(context.Background(), domain.SystemAdminContextUserInfo())
|
||||||
@@ -132,6 +142,12 @@ func (m Manager) handleUserLoginEvent(userId domain.UserIdentifier) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, loaded := m.userLockMap.LoadOrStore(userId, "login")
|
||||||
|
if loaded {
|
||||||
|
return // another goroutine is already handling this user
|
||||||
|
}
|
||||||
|
defer m.userLockMap.Delete(userId)
|
||||||
|
|
||||||
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to retrieve existing peers prior to default peer creation",
|
slog.Error("failed to retrieve existing peers prior to default peer creation",
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
"github.com/h44z/wg-portal/internal/app"
|
||||||
@@ -23,12 +24,24 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userPeers, err := m.db.GetUserPeers(context.Background(), userId)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
if iface.Type != domain.InterfaceTypeServer {
|
if iface.Type != domain.InterfaceTypeServer {
|
||||||
continue // only create default peers for server interfaces
|
continue // only create default peers for server interfaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
peer, err := m.PreparePeer(ctx, iface.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
|
return fmt.Errorf("failed to create default peer for interface %s: %w", iface.Identifier, err)
|
||||||
@@ -220,7 +233,7 @@ func (m Manager) CreateMultiplePeers(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPeers []*domain.Peer
|
createdPeers := make([]domain.Peer, 0, len(r.UserIdentifiers))
|
||||||
|
|
||||||
for _, id := range r.UserIdentifiers {
|
for _, id := range r.UserIdentifiers {
|
||||||
freshPeer, err := m.PreparePeer(ctx, interfaceId)
|
freshPeer, err := m.PreparePeer(ctx, interfaceId)
|
||||||
@@ -229,27 +242,22 @@ func (m Manager) CreateMultiplePeers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
|
freshPeer.UserIdentifier = domain.UserIdentifier(id) // use id as user identifier. peers are allowed to have invalid user identifiers
|
||||||
if r.Suffix != "" {
|
if r.Prefix != "" {
|
||||||
freshPeer.DisplayName += " " + r.Suffix
|
freshPeer.DisplayName = r.Prefix + " " + freshPeer.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
|
if err := m.validatePeerCreation(ctx, nil, freshPeer); err != nil {
|
||||||
return nil, fmt.Errorf("creation not allowed: %w", err)
|
return nil, fmt.Errorf("creation not allowed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newPeers = append(newPeers, freshPeer)
|
// Save immediately to reserve the assigned IPs so the next prepared peer gets the next free IPs
|
||||||
}
|
if err := m.savePeers(ctx, freshPeer); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new peer %s: %w", freshPeer.Identifier, err)
|
||||||
|
}
|
||||||
|
|
||||||
err := m.savePeers(ctx, newPeers...)
|
createdPeers = append(createdPeers, *freshPeer)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create new peers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
createdPeers := make([]domain.Peer, len(newPeers))
|
m.bus.Publish(app.TopicPeerCreated, *freshPeer)
|
||||||
for i := range newPeers {
|
|
||||||
createdPeers[i] = *newPeers[i]
|
|
||||||
|
|
||||||
m.bus.Publish(app.TopicPeerCreated, *newPeers[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdPeers, nil
|
return createdPeers, nil
|
||||||
|
@@ -190,6 +190,8 @@ func GetConfig() (*Config, error) {
|
|||||||
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
return nil, fmt.Errorf("failed to load config from yaml: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Web.Sanitize()
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// WebConfig contains the configuration for the web server.
|
// WebConfig contains the configuration for the web server.
|
||||||
type WebConfig struct {
|
type WebConfig struct {
|
||||||
// RequestLogging enables logging of all HTTP requests.
|
// RequestLogging enables logging of all HTTP requests.
|
||||||
@@ -26,3 +28,7 @@ type WebConfig struct {
|
|||||||
// KeyFile is the path to the TLS certificate key file.
|
// KeyFile is the path to the TLS certificate key file.
|
||||||
KeyFile string `yaml:"key_file"`
|
KeyFile string `yaml:"key_file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WebConfig) Sanitize() {
|
||||||
|
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
||||||
|
}
|
||||||
|
@@ -136,6 +136,7 @@ func (p *Peer) OverwriteUserEditableFields(userPeer *Peer, cfg *config.Config) {
|
|||||||
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
p.Interface.PublicKey = userPeer.Interface.PublicKey
|
||||||
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
p.Interface.PrivateKey = userPeer.Interface.PrivateKey
|
||||||
p.PresharedKey = userPeer.PresharedKey
|
p.PresharedKey = userPeer.PresharedKey
|
||||||
|
p.Identifier = userPeer.Identifier
|
||||||
}
|
}
|
||||||
p.Interface.Mtu = userPeer.Interface.Mtu
|
p.Interface.Mtu = userPeer.Interface.Mtu
|
||||||
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
p.PersistentKeepalive = userPeer.PersistentKeepalive
|
||||||
@@ -268,5 +269,5 @@ func MergeToPhysicalPeer(pp *PhysicalPeer, p *Peer) {
|
|||||||
|
|
||||||
type PeerCreationRequest struct {
|
type PeerCreationRequest struct {
|
||||||
UserIdentifiers []string
|
UserIdentifiers []string
|
||||||
Suffix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user