mirror of
https://github.com/h44z/wg-portal.git
synced 2026-02-23 10:56:22 +00:00
Compare commits
2 Commits
web_base_p
...
bulk_actio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4968dcd6c | ||
|
|
0a88fe745f |
@@ -84,7 +84,7 @@ func main() {
|
|||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
userManager.StartBackgroundJobs(ctx)
|
userManager.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, eventBus, userManager)
|
authenticator, err := auth.NewAuthenticator(&cfg.Auth, cfg.Web.ExternalUrl, cfg.Web.BasePath, eventBus, userManager)
|
||||||
internal.AssertNoError(err)
|
internal.AssertNoError(err)
|
||||||
authenticator.StartBackgroundJobs(ctx)
|
authenticator.StartBackgroundJobs(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,11 @@ core:
|
|||||||
|
|
||||||
web:
|
web:
|
||||||
external_url: http://localhost:8888
|
external_url: http://localhost:8888
|
||||||
|
base_path: ""
|
||||||
request_logging: true
|
request_logging: true
|
||||||
# Optional path where custom frontend files are stored.
|
|
||||||
# If this folder contains at least one file, it will override the embedded frontend.
|
|
||||||
# If the folder is empty or does not exist on startup, the embedded frontend will be
|
|
||||||
# written into it. Leave empty to use the embedded frontend only.
|
|
||||||
frontend_filepath: ""
|
frontend_filepath: ""
|
||||||
|
|
||||||
mail:
|
mail:
|
||||||
# Path where custom email templates (.gotpl and .gohtml) are stored.
|
|
||||||
# If the directory is empty on startup, the default embedded templates
|
|
||||||
# will be written there so you can modify them.
|
|
||||||
# Leave empty to use embedded templates only.
|
|
||||||
templates_path: ""
|
templates_path: ""
|
||||||
|
|
||||||
webhook:
|
webhook:
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ auth:
|
|||||||
web:
|
web:
|
||||||
listening_address: :8888
|
listening_address: :8888
|
||||||
external_url: http://localhost:8888
|
external_url: http://localhost:8888
|
||||||
|
base_path: ""
|
||||||
site_company_name: WireGuard Portal
|
site_company_name: WireGuard Portal
|
||||||
site_title: WireGuard Portal
|
site_title: WireGuard Portal
|
||||||
session_identifier: wgPortalSession
|
session_identifier: wgPortalSession
|
||||||
@@ -801,8 +802,15 @@ Without a valid `external_url`, the login process may fail due to CSRF protectio
|
|||||||
- **Default:** `http://localhost:8888`
|
- **Default:** `http://localhost:8888`
|
||||||
- **Environment Variable:** `WG_PORTAL_WEB_EXTERNAL_URL`
|
- **Environment Variable:** `WG_PORTAL_WEB_EXTERNAL_URL`
|
||||||
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
- **Description:** The URL where a client can access WireGuard Portal. This URL is used for generating links in emails and for performing OAUTH redirects.
|
||||||
|
The external URL must not contain a path component or trailing slash. If you want to serve WireGuard Portal on a subpath, use the `base_path` setting.
|
||||||
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
**Important:** If you are using a reverse proxy, set this to the external URL of the reverse proxy, otherwise login will fail. If you access the portal via IP address, set this to the IP address of the server.
|
||||||
|
|
||||||
|
### `base_path`
|
||||||
|
- **Default:** *(empty)*
|
||||||
|
- **Environment Variable:** `WG_PORTAL_WEB_BASE_PATH`
|
||||||
|
- **Description:** The base path for the web server (e.g., `/wgportal`).
|
||||||
|
By default (meaning an empty value), the portal will be served from the root path `/`.
|
||||||
|
|
||||||
### `site_company_name`
|
### `site_company_name`
|
||||||
- **Default:** `WireGuard Portal`
|
- **Default:** `WireGuard Portal`
|
||||||
- **Environment Variable:** `WG_PORTAL_WEB_SITE_COMPANY_NAME`
|
- **Environment Variable:** `WG_PORTAL_WEB_SITE_COMPANY_NAME`
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ web:
|
|||||||
external_url: https://wg.domain.com
|
external_url: https://wg.domain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to serve the web interface on a different base-path, you can also set the `web.base_path` option:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
web:
|
||||||
|
external_url: https://wg.domain.com
|
||||||
|
base_path: /subpath
|
||||||
|
```
|
||||||
|
|
||||||
|
The WireGuard Portal will then be available at `https://wg.domain.com/subpath`.
|
||||||
|
|
||||||
### Built-in TLS
|
### Built-in TLS
|
||||||
|
|
||||||
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
If you prefer to let WireGuard Portal handle TLS itself, you can use the built-in TLS support.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// global config, will be overridden by backend if available
|
// global config, will be overridden by backend if available
|
||||||
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
let WGPORTAL_BACKEND_BASE_URL="http://localhost:5000/api/v0";
|
||||||
|
let WGPORTAL_BASE_PATH="";
|
||||||
let WGPORTAL_VERSION="unknown";
|
let WGPORTAL_VERSION="unknown";
|
||||||
let WGPORTAL_SITE_TITLE="WireGuard Portal";
|
let WGPORTAL_SITE_TITLE="WireGuard Portal";
|
||||||
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
|
let WGPORTAL_SITE_COMPANY_NAME="WireGuard Portal";
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ const languageFlag = computed(() => {
|
|||||||
const companyName = ref(WGPORTAL_SITE_COMPANY_NAME);
|
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 webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||||
|
|
||||||
const userDisplayName = computed(() => {
|
const userDisplayName = computed(() => {
|
||||||
let displayName = "Unknown";
|
let displayName = "Unknown";
|
||||||
@@ -113,7 +114,7 @@ const userDisplayName = computed(() => {
|
|||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="/"><img :alt="companyName" src="/img/header-logo.png" /></a>
|
<RouterLink class="navbar-brand" :to="{ name: 'home' }"><img :alt="companyName" :src="webBasePath + '/img/header-logo.png'" /></RouterLink>
|
||||||
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
<button aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler"
|
||||||
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
data-bs-target="#navbarTop" data-bs-toggle="collapse" type="button">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"button-add-peers": "Mehrere Peers hinzufügen",
|
"button-add-peers": "Mehrere Peers hinzufügen",
|
||||||
"button-show-peer": "Peer anzeigen",
|
"button-show-peer": "Peer anzeigen",
|
||||||
"button-edit-peer": "Peer bearbeiten",
|
"button-edit-peer": "Peer bearbeiten",
|
||||||
|
"button-bulk-delete": "Ausgewählte Peers löschen",
|
||||||
|
"button-bulk-enable": "Ausgewählte Peers aktivieren",
|
||||||
|
"button-bulk-disable": "Ausgewählte Peers deaktivieren",
|
||||||
|
"confirm-bulk-delete": "Sind Sie sicher, dass Sie {count} Peers löschen möchten?",
|
||||||
|
"confirm-bulk-disable": "Sind Sie sicher, dass Sie {count} Peers deaktivieren möchten?",
|
||||||
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
"peer-disabled": "Peer ist deaktiviert, Grund:",
|
||||||
"peer-expiring": "Peer läuft ab am",
|
"peer-expiring": "Peer läuft ab am",
|
||||||
"peer-connected": "Verbunden",
|
"peer-connected": "Verbunden",
|
||||||
@@ -153,6 +158,14 @@
|
|||||||
"button-add-user": "Benutzer hinzufügen",
|
"button-add-user": "Benutzer hinzufügen",
|
||||||
"button-show-user": "Benutzer anzeigen",
|
"button-show-user": "Benutzer anzeigen",
|
||||||
"button-edit-user": "Benutzer bearbeiten",
|
"button-edit-user": "Benutzer bearbeiten",
|
||||||
|
"button-bulk-delete": "Ausgewählte Benutzer löschen",
|
||||||
|
"button-bulk-enable": "Ausgewählte Benutzer aktivieren",
|
||||||
|
"button-bulk-disable": "Ausgewählte Benutzer deaktivieren",
|
||||||
|
"button-bulk-lock": "Ausgewählte Benutzer sperren",
|
||||||
|
"button-bulk-unlock": "Ausgewählte Benutzer entsperren",
|
||||||
|
"confirm-bulk-delete": "Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?",
|
||||||
|
"confirm-bulk-disable": "Sind Sie sicher, dass Sie {count} Benutzer deaktivieren möchten?",
|
||||||
|
"confirm-bulk-lock": "Sind Sie sicher, dass Sie {count} Benutzer sperren möchten?",
|
||||||
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
"user-disabled": "Benutzer ist deaktiviert, Grund:",
|
||||||
"user-locked": "Konto ist gesperrt, Grund:",
|
"user-locked": "Konto ist gesperrt, Grund:",
|
||||||
"admin": "Benutzer hat Administratorrechte",
|
"admin": "Benutzer hat Administratorrechte",
|
||||||
|
|||||||
@@ -129,6 +129,11 @@
|
|||||||
"button-add-peers": "Add Multiple Peers",
|
"button-add-peers": "Add Multiple Peers",
|
||||||
"button-show-peer": "Show Peer",
|
"button-show-peer": "Show Peer",
|
||||||
"button-edit-peer": "Edit Peer",
|
"button-edit-peer": "Edit Peer",
|
||||||
|
"button-bulk-delete": "Delete selected peers",
|
||||||
|
"button-bulk-enable": "Enable selected peers",
|
||||||
|
"button-bulk-disable": "Disable selected peers",
|
||||||
|
"confirm-bulk-delete": "Are you sure you want to delete {count} peers?",
|
||||||
|
"confirm-bulk-disable": "Are you sure you want to disable {count} peers?",
|
||||||
"peer-disabled": "Peer is disabled, reason:",
|
"peer-disabled": "Peer is disabled, reason:",
|
||||||
"peer-expiring": "Peer is expiring at",
|
"peer-expiring": "Peer is expiring at",
|
||||||
"peer-connected": "Connected",
|
"peer-connected": "Connected",
|
||||||
@@ -153,6 +158,14 @@
|
|||||||
"button-add-user": "Add User",
|
"button-add-user": "Add User",
|
||||||
"button-show-user": "Show User",
|
"button-show-user": "Show User",
|
||||||
"button-edit-user": "Edit User",
|
"button-edit-user": "Edit User",
|
||||||
|
"button-bulk-delete": "Delete selected users",
|
||||||
|
"button-bulk-enable": "Enable selected users",
|
||||||
|
"button-bulk-disable": "Disable selected users",
|
||||||
|
"button-bulk-lock": "Lock selected users",
|
||||||
|
"button-bulk-unlock": "Unlock selected users",
|
||||||
|
"confirm-bulk-delete": "Are you sure you want to delete {count} users?",
|
||||||
|
"confirm-bulk-disable": "Are you sure you want to disable {count} users?",
|
||||||
|
"confirm-bulk-lock": "Are you sure you want to lock {count} users?",
|
||||||
"user-disabled": "User is disabled, reason:",
|
"user-disabled": "User is disabled, reason:",
|
||||||
"user-locked": "Account is locked, reason:",
|
"user-locked": "Account is locked, reason:",
|
||||||
"admin": "User has administrator privileges",
|
"admin": "User has administrator privileges",
|
||||||
|
|||||||
@@ -222,6 +222,73 @@ export const peerStore = defineStore('peers', {
|
|||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async BulkDelete(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-delete`, { Identifiers: ids })
|
||||||
|
.then(() => {
|
||||||
|
this.peers = this.peers.filter(p => !ids.includes(p.Identifier))
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Peers deleted",
|
||||||
|
text: "Selected peers have been deleted!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to delete peers: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to delete selected peers!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkEnable(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-enable`, { Identifiers: ids })
|
||||||
|
.then(async () => {
|
||||||
|
await this.LoadPeers()
|
||||||
|
notify({
|
||||||
|
title: "Peers enabled",
|
||||||
|
text: "Selected peers have been enabled!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to enable peers: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to enable selected peers!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkDisable(ids, reason) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-disable`, { Identifiers: ids, Reason: reason })
|
||||||
|
.then(async () => {
|
||||||
|
await this.LoadPeers()
|
||||||
|
notify({
|
||||||
|
title: "Peers disabled",
|
||||||
|
text: "Selected peers have been disabled!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to disable peers: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to disable selected peers!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
async UpdatePeer(id, formData) {
|
async UpdatePeer(id, formData) {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
|||||||
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
import {apiWrapper} from "@/helpers/fetch-wrapper";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
import {authStore} from "@/stores/auth";
|
import {authStore} from "@/stores/auth";
|
||||||
|
import {peerStore} from "@/stores/peers";
|
||||||
import { base64_url_encode } from '@/helpers/encoding';
|
import { base64_url_encode } from '@/helpers/encoding';
|
||||||
import {freshStats} from "@/helpers/models";
|
import {freshStats} from "@/helpers/models";
|
||||||
import { ipToBigInt } from '@/helpers/utils';
|
import { ipToBigInt } from '@/helpers/utils';
|
||||||
@@ -218,5 +219,18 @@ export const profileStore = defineStore('profile', {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async BulkDelete(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
const peers = peerStore()
|
||||||
|
return peers.BulkDelete(ids)
|
||||||
|
.then(() => {
|
||||||
|
this.peers = this.peers.filter(p => !ids.includes(p.Identifier))
|
||||||
|
this.fetching = false
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -142,5 +142,140 @@ export const userStore = defineStore('users', {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async BulkDelete(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-delete`, { Identifiers: ids })
|
||||||
|
.then(() => {
|
||||||
|
this.users = this.users.filter(u => !ids.includes(u.Identifier))
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Users deleted",
|
||||||
|
text: "Selected users have been deleted!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to delete users: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to delete selected users!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkEnable(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-enable`, { Identifiers: ids })
|
||||||
|
.then(() => {
|
||||||
|
this.users.forEach(u => {
|
||||||
|
if (ids.includes(u.Identifier)) {
|
||||||
|
u.Disabled = false
|
||||||
|
u.DisabledReason = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Users enabled",
|
||||||
|
text: "Selected users have been enabled!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to enable users: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to enable selected users!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkDisable(ids, reason) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-disable`, { Identifiers: ids, Reason: reason })
|
||||||
|
.then(() => {
|
||||||
|
this.users.forEach(u => {
|
||||||
|
if (ids.includes(u.Identifier)) {
|
||||||
|
u.Disabled = true
|
||||||
|
u.DisabledReason = reason
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Users disabled",
|
||||||
|
text: "Selected users have been disabled!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to disable users: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to disable selected users!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkLock(ids, reason) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-lock`, { Identifiers: ids, Reason: reason })
|
||||||
|
.then(() => {
|
||||||
|
this.users.forEach(u => {
|
||||||
|
if (ids.includes(u.Identifier)) {
|
||||||
|
u.Locked = true
|
||||||
|
u.LockedReason = reason
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Users locked",
|
||||||
|
text: "Selected users have been locked!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to lock users: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to lock selected users!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async BulkUnlock(ids) {
|
||||||
|
this.fetching = true
|
||||||
|
return apiWrapper.post(`${baseUrl}/bulk-unlock`, { Identifiers: ids })
|
||||||
|
.then(() => {
|
||||||
|
this.users.forEach(u => {
|
||||||
|
if (ids.includes(u.Identifier)) {
|
||||||
|
u.Locked = false
|
||||||
|
u.LockedReason = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.fetching = false
|
||||||
|
notify({
|
||||||
|
title: "Users unlocked",
|
||||||
|
text: "Selected users have been unlocked!",
|
||||||
|
type: 'success',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.fetching = false
|
||||||
|
console.log("Failed to unlock users: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to unlock selected users!",
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
throw new Error(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ const sortKey = ref("")
|
|||||||
const sortOrder = ref(1)
|
const sortOrder = ref(1)
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
|
|
||||||
|
const selectedPeers = computed(() => {
|
||||||
|
return peers.All.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
|
||||||
|
})
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
||||||
@@ -111,6 +115,39 @@ async function saveConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkDelete() {
|
||||||
|
if (confirm(t('interfaces.confirm-bulk-delete', {count: selectedPeers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await peers.BulkDelete(selectedPeers.value)
|
||||||
|
selectAll.value = false // reset selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkEnable() {
|
||||||
|
try {
|
||||||
|
await peers.BulkEnable(selectedPeers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
peers.All.forEach(p => p.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkDisable() {
|
||||||
|
if (confirm(t('interfaces.confirm-bulk-disable', {count: selectedPeers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await peers.BulkDisable(selectedPeers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
peers.All.forEach(p => p.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
peers.FilteredAndPaged.forEach(peer => {
|
peers.FilteredAndPaged.forEach(peer => {
|
||||||
peer.IsSelected = selectAll.value;
|
peer.IsSelected = selectAll.value;
|
||||||
@@ -353,6 +390,13 @@ onMounted(async () => {
|
|||||||
<a class="btn btn-primary ms-2" href="#" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
<a class="btn btn-primary ms-2" href="#" :title="$t('interfaces.button-add-peer')" @click.prevent="editPeerId='#NEW#'"><i class="fa fa-plus me-1"></i><i class="fa fa-user"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" v-if="selectedPeers.length > 0">
|
||||||
|
<div class="col-12 text-lg-end">
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-enable')" @click.prevent="bulkEnable"><i class="fa-regular fa-circle-check"></i></a>
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-disable')" @click.prevent="bulkDisable"><i class="fa fa-ban"></i></a>
|
||||||
|
<a class="btn btn-outline-danger btn-sm ms-2" href="#" :title="$t('interfaces.button-bulk-delete')" @click.prevent="bulkDelete"><i class="fa fa-trash-can"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="interfaces.Count!==0" class="mt-2 table-responsive">
|
<div v-if="interfaces.Count!==0" class="mt-2 table-responsive">
|
||||||
<div v-if="peers.Count===0">
|
<div v-if="peers.Count===0">
|
||||||
<h4>{{ $t('interfaces.no-peer.headline') }}</h4>
|
<h4>{{ $t('interfaces.no-peer.headline') }}</h4>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PeerViewModal from "../components/PeerViewModal.vue";
|
import PeerViewModal from "../components/PeerViewModal.vue";
|
||||||
|
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref, computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { profileStore } from "@/stores/profile";
|
import { profileStore } from "@/stores/profile";
|
||||||
|
import { peerStore } from "@/stores/peers";
|
||||||
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
|
||||||
import { settingsStore } from "@/stores/settings";
|
import { settingsStore } from "@/stores/settings";
|
||||||
import { humanFileSize } from "@/helpers/utils";
|
import { humanFileSize } from "@/helpers/utils";
|
||||||
|
|
||||||
const settings = settingsStore()
|
const settings = settingsStore()
|
||||||
const profile = profileStore()
|
const profile = profileStore()
|
||||||
|
const peers = peerStore()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const viewedPeerId = ref("")
|
const viewedPeerId = ref("")
|
||||||
const editPeerId = ref("")
|
const editPeerId = ref("")
|
||||||
@@ -17,6 +22,10 @@ const sortKey = ref("")
|
|||||||
const sortOrder = ref(1)
|
const sortOrder = ref(1)
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
|
|
||||||
|
const selectedPeers = computed(() => {
|
||||||
|
return profile.Peers.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
|
||||||
|
})
|
||||||
|
|
||||||
function sortBy(key) {
|
function sortBy(key) {
|
||||||
if (sortKey.value === key) {
|
if (sortKey.value === key) {
|
||||||
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
sortOrder.value = sortOrder.value * -1; // Toggle sort order
|
||||||
@@ -35,6 +44,17 @@ function friendlyInterfaceName(id, name) {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkDelete() {
|
||||||
|
if (confirm(t('interfaces.confirm-bulk-delete', {count: selectedPeers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await profile.BulkDelete(selectedPeers.value)
|
||||||
|
selectAll.value = false // reset selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
profile.FilteredAndPagedPeers.forEach(peer => {
|
profile.FilteredAndPagedPeers.forEach(peer => {
|
||||||
peer.IsSelected = selectAll.value;
|
peer.IsSelected = selectAll.value;
|
||||||
@@ -84,6 +104,13 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" v-if="selectedPeers.length > 0">
|
||||||
|
<div class="col-12 text-lg-end">
|
||||||
|
<button class="btn btn-outline-danger btn-sm" :title="$t('interfaces.button-bulk-delete')" @click.prevent="bulkDelete">
|
||||||
|
<i class="fa fa-trash-can"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mt-2 table-responsive">
|
<div class="mt-2 table-responsive">
|
||||||
<div v-if="profile.CountPeers === 0">
|
<div v-if="profile.CountPeers === 0">
|
||||||
<h4>{{ $t('profile.no-peer.headline') }}</h4>
|
<h4>{{ $t('profile.no-peer.headline') }}</h4>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const profile = profileStore()
|
|||||||
const settings = settingsStore()
|
const settings = settingsStore()
|
||||||
const auth = authStore()
|
const auth = authStore()
|
||||||
|
|
||||||
|
const webBasePath = ref(WGPORTAL_BASE_PATH);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await profile.LoadUser()
|
await profile.LoadUser()
|
||||||
await auth.LoadWebAuthnCredentials()
|
await auth.LoadWebAuthnCredentials()
|
||||||
@@ -241,7 +243,7 @@ const updatePassword = async () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="/api/v1/doc.html" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
<a :href="webBasePath + '/api/v1/doc.html'" target="_blank" :alt="$t('settings.api.api-link')">{{ $t('settings.api.api-link') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,77 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {userStore} from "@/stores/users";
|
import {userStore} from "@/stores/users";
|
||||||
import {ref,onMounted} 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 {useI18n} from "vue-i18n";
|
||||||
|
|
||||||
const users = userStore()
|
const users = userStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const editUserId = ref("")
|
const editUserId = ref("")
|
||||||
const viewedUserId = ref("")
|
const viewedUserId = ref("")
|
||||||
|
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
|
|
||||||
|
const selectedUsers = computed(() => {
|
||||||
|
return users.All.filter(user => user.IsSelected).map(user => user.Identifier);
|
||||||
|
})
|
||||||
|
|
||||||
|
async function bulkDelete() {
|
||||||
|
if (confirm(t('users.confirm-bulk-delete', {count: selectedUsers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await users.BulkDelete(selectedUsers.value)
|
||||||
|
selectAll.value = false // reset selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkEnable() {
|
||||||
|
try {
|
||||||
|
await users.BulkEnable(selectedUsers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkDisable() {
|
||||||
|
if (confirm(t('users.confirm-bulk-disable', {count: selectedUsers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await users.BulkDisable(selectedUsers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkLock() {
|
||||||
|
if (confirm(t('users.confirm-bulk-lock', {count: selectedUsers.value.length}))) {
|
||||||
|
try {
|
||||||
|
await users.BulkLock(selectedUsers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bulkUnlock() {
|
||||||
|
try {
|
||||||
|
await users.BulkUnlock(selectedUsers.value)
|
||||||
|
selectAll.value = false
|
||||||
|
users.All.forEach(u => u.IsSelected = false) // remove selection
|
||||||
|
} catch (e) {
|
||||||
|
// notification is handled in store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
users.FilteredAndPaged.forEach(user => {
|
users.FilteredAndPaged.forEach(user => {
|
||||||
user.IsSelected = selectAll.value;
|
user.IsSelected = selectAll.value;
|
||||||
@@ -45,6 +106,15 @@ onMounted(() => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" v-if="selectedUsers.length > 0">
|
||||||
|
<div class="col-12 text-lg-end">
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-enable')" @click.prevent="bulkEnable"><i class="fa-regular fa-circle-check"></i></a>
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-disable')" @click.prevent="bulkDisable"><i class="fa fa-ban"></i></a>
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-unlock')" @click.prevent="bulkUnlock"><i class="fa-solid fa-lock-open"></i></a>
|
||||||
|
<a class="btn btn-outline-primary btn-sm ms-2" href="#" :title="$t('users.button-bulk-lock')" @click.prevent="bulkLock"><i class="fa-solid fa-lock"></i></a>
|
||||||
|
<a class="btn btn-outline-danger btn-sm ms-2" href="#" :title="$t('users.button-bulk-delete')" @click.prevent="bulkDelete"><i class="fa fa-trash-can"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="mt-2 table-responsive">
|
<div class="mt-2 table-responsive">
|
||||||
<div v-if="users.Count===0">
|
<div v-if="users.Count===0">
|
||||||
<h4>{{ $t('users.no-user.headline') }}</h4>
|
<h4>{{ $t('users.no-user.headline') }}</h4>
|
||||||
|
|||||||
@@ -800,6 +800,126 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/peer/bulk-delete": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Bulk delete selected peers.",
|
||||||
|
"operationId": "peers_handleBulkDelete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of peer identifiers to delete",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if deletion was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/peer/bulk-disable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Bulk disable selected peers.",
|
||||||
|
"operationId": "peers_handleBulkDisable",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of peer identifiers to disable",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/peer/bulk-enable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Peer"
|
||||||
|
],
|
||||||
|
"summary": "Bulk enable selected peers.",
|
||||||
|
"operationId": "peers_handleBulkEnable",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of peer identifiers to enable",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/peer/config-mail": {
|
"/peer/config-mail": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1324,6 +1444,206 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/user/bulk-delete": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Bulk delete selected users.",
|
||||||
|
"operationId": "users_handleBulkDelete",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of user identifiers to delete",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if deletion was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/bulk-disable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Bulk disable selected users.",
|
||||||
|
"operationId": "users_handleBulkDisable",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of user identifiers to disable",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/bulk-enable": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Bulk enable selected users.",
|
||||||
|
"operationId": "users_handleBulkEnable",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of user identifiers to enable",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/bulk-lock": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Bulk lock selected users.",
|
||||||
|
"operationId": "users_handleBulkLock",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of user identifiers to lock",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/user/bulk-unlock": {
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Bulk unlock selected users.",
|
||||||
|
"operationId": "users_handleBulkUnlock",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "A list of user identifiers to unlock",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.BulkPeerRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No content if action was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/user/new": {
|
"/user/new": {
|
||||||
"post": {
|
"post": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -1737,6 +2057,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"model.BulkPeerRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"Identifiers"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Identifiers": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Reason": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"model.ConfigOption-array_string": {
|
"model.ConfigOption-array_string": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ definitions:
|
|||||||
Timestamp:
|
Timestamp:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
model.BulkPeerRequest:
|
||||||
|
properties:
|
||||||
|
Identifiers:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
Reason:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- Identifiers
|
||||||
|
type: object
|
||||||
model.ConfigOption-array_string:
|
model.ConfigOption-array_string:
|
||||||
properties:
|
properties:
|
||||||
Overridable:
|
Overridable:
|
||||||
@@ -1080,6 +1091,84 @@ paths:
|
|||||||
summary: Update the given peer record.
|
summary: Update the given peer record.
|
||||||
tags:
|
tags:
|
||||||
- Peer
|
- Peer
|
||||||
|
/peer/bulk-delete:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleBulkDelete
|
||||||
|
parameters:
|
||||||
|
- description: A list of peer identifiers to delete
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if deletion was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk delete selected peers.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
|
/peer/bulk-disable:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleBulkDisable
|
||||||
|
parameters:
|
||||||
|
- description: A list of peer identifiers to disable
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk disable selected peers.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
|
/peer/bulk-enable:
|
||||||
|
post:
|
||||||
|
operationId: peers_handleBulkEnable
|
||||||
|
parameters:
|
||||||
|
- description: A list of peer identifiers to enable
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk enable selected peers.
|
||||||
|
tags:
|
||||||
|
- Peer
|
||||||
/peer/config-mail:
|
/peer/config-mail:
|
||||||
post:
|
post:
|
||||||
operationId: peers_handleEmailPost
|
operationId: peers_handleEmailPost
|
||||||
@@ -1571,6 +1660,136 @@ paths:
|
|||||||
summary: Get all user records.
|
summary: Get all user records.
|
||||||
tags:
|
tags:
|
||||||
- Users
|
- Users
|
||||||
|
/user/bulk-delete:
|
||||||
|
post:
|
||||||
|
operationId: users_handleBulkDelete
|
||||||
|
parameters:
|
||||||
|
- description: A list of user identifiers to delete
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if deletion was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk delete selected users.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/bulk-disable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleBulkDisable
|
||||||
|
parameters:
|
||||||
|
- description: A list of user identifiers to disable
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk disable selected users.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/bulk-enable:
|
||||||
|
post:
|
||||||
|
operationId: users_handleBulkEnable
|
||||||
|
parameters:
|
||||||
|
- description: A list of user identifiers to enable
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk enable selected users.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/bulk-lock:
|
||||||
|
post:
|
||||||
|
operationId: users_handleBulkLock
|
||||||
|
parameters:
|
||||||
|
- description: A list of user identifiers to lock
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk lock selected users.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
/user/bulk-unlock:
|
||||||
|
post:
|
||||||
|
operationId: users_handleBulkUnlock
|
||||||
|
parameters:
|
||||||
|
- description: A list of user identifiers to unlock
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.BulkPeerRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: No content if action was successful
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Error'
|
||||||
|
summary: Bulk unlock selected users.
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
/user/new:
|
/user/new:
|
||||||
post:
|
post:
|
||||||
operationId: users_handleCreatePost
|
operationId: users_handleCreatePost
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 709 B |
@@ -6,9 +6,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>WireGuard Portal API</title>
|
<title>WireGuard Portal API</title>
|
||||||
<meta name="description" content="WireGuard Portal API">
|
<meta name="description" content="WireGuard Portal API">
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="{{$.BasePath}}/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="/fonts/fontawesome-all.min.css">
|
<link rel="stylesheet" href="{{$.BasePath}}/fonts/fontawesome-all.min.css">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/app/favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="{{$.BasePath}}/app/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="page-top" class="d-flex flex-column min-vh-100">
|
<body id="page-top" class="d-flex flex-column min-vh-100">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="card-header">SPA Api</div>
|
<div class="card-header">SPA Api</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">This endpoint is used by the Single Page Application (the Frontend/UI).</p>
|
<p class="card-text">This endpoint is used by the Single Page Application (the Frontend/UI).</p>
|
||||||
<a href="/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
<a href="{{$.BasePath}}/api/v0/doc.html" title="API version 0" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="card-header"><b>Version 1</b></div>
|
<div class="card-header"><b>Version 1</b></div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">This is the current main API endpoint.</p>
|
<p class="card-text">This is the current main API endpoint.</p>
|
||||||
<a href="/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
<a href="{{$.BasePath}}/api/v1/doc.html" title="API version 1" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,17 +43,17 @@
|
|||||||
<div class="card-header">Version 2</div>
|
<div class="card-header">Version 2</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text">This will be a future API version, it is currently work in progress.</p>
|
<p class="card-text">This will be a future API version, it is currently work in progress.</p>
|
||||||
<a href="/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
<a href="{{$.BasePath}}/api/v2/doc.html" title="API version 2" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">Open Documentation</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{template "prt_footer.gohtml" .}}
|
{{template "prt_footer.gohtml" .}}
|
||||||
<script src="/js/jquery.min.js"></script>
|
<script src="{{$.BasePath}}/js/jquery.min.js"></script>
|
||||||
<script src="/js/jquery.easing.js"></script>
|
<script src="{{$.BasePath}}/js/jquery.easing.js"></script>
|
||||||
<script src="/js/popper.min.js"></script>
|
<script src="{{$.BasePath}}/js/popper.min.js"></script>
|
||||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
<script src="{{$.BasePath}}/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a class="navbar-brand" href="/"><img src="/img/header-logo.png" alt="Prolicht"/></a>
|
<a class="navbar-brand" href="/"><img src="{{$.BasePath}}/img/header-logo.png" alt="Prolicht"/></a>
|
||||||
<div id="topNavbar" class="navbar-collapse collapse">
|
<div id="topNavbar" class="navbar-collapse collapse">
|
||||||
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
allow-spec-file-load="false"
|
allow-spec-file-load="false"
|
||||||
allow-spec-file-download="true"
|
allow-spec-file-download="true"
|
||||||
>
|
>
|
||||||
<img slot="logo" src="/img/header-logo-small.png" style="width:50px; height:50px"/>
|
<img slot="logo" src="{{$.BasePath}}/img/header-logo-small.png" style="width:50px; height:50px"/>
|
||||||
|
|
||||||
<p slot="footer" style="margin:0; padding:16px 36px; background-color:#f76b39; color:#fff; text-align:center;" >
|
<p slot="footer" style="margin:0; padding:16px 36px; background-color:#f76b39; color:#fff; text-align:center;" >
|
||||||
Copyright © WireGuard Portal {{$.Year}}, version {{$.Version}}
|
Copyright © WireGuard Portal {{$.Year}}, version {{$.Version}}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
@@ -37,6 +39,7 @@ type ApiEndpointSetupFunc func() (ApiVersion, GroupSetupFn)
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
server *routegroup.Bundle
|
server *routegroup.Bundle
|
||||||
|
root *routegroup.Bundle // root is the web-root (potentially with path prefix)
|
||||||
tpl *respond.TemplateRenderer
|
tpl *respond.TemplateRenderer
|
||||||
versions map[ApiVersion]*routegroup.Bundle
|
versions map[ApiVersion]*routegroup.Bundle
|
||||||
}
|
}
|
||||||
@@ -77,13 +80,42 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
|
|||||||
template.Must(template.New("").ParseFS(apiTemplates, "assets/tpl/*.gohtml")),
|
template.Must(template.New("").ParseFS(apiTemplates, "assets/tpl/*.gohtml")),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Serve static files
|
// Mount base path if configured
|
||||||
|
s.root = s.server
|
||||||
|
if s.cfg.Web.BasePath != "" {
|
||||||
|
s.root = s.server.Mount(s.cfg.Web.BasePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files (under base path if configured)
|
||||||
imgFs := http.FS(fsMust(fs.Sub(apiStatics, "assets/img")))
|
imgFs := http.FS(fsMust(fs.Sub(apiStatics, "assets/img")))
|
||||||
s.server.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
s.root.HandleFiles("/css", http.FS(fsMust(fs.Sub(apiStatics, "assets/css"))))
|
||||||
s.server.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
s.root.HandleFiles("/js", http.FS(fsMust(fs.Sub(apiStatics, "assets/js"))))
|
||||||
s.server.HandleFiles("/img", imgFs)
|
s.root.HandleFiles("/img", imgFs)
|
||||||
s.server.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
s.root.HandleFiles("/fonts", http.FS(fsMust(fs.Sub(apiStatics, "assets/fonts"))))
|
||||||
s.server.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
if cfg.Web.BasePath == "" {
|
||||||
|
s.root.HandleFiles("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
|
||||||
|
} else {
|
||||||
|
customV0File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v0_swagger.yaml")
|
||||||
|
customV1File, _ := fs.ReadFile(fsMust(fs.Sub(apiStatics, "assets/doc")), "v1_swagger.yaml")
|
||||||
|
customV0File = []byte(strings.Replace(string(customV0File),
|
||||||
|
"basePath: /api/v0", "basePath: "+cfg.Web.BasePath+"/api/v0", 1))
|
||||||
|
customV1File = []byte(strings.Replace(string(customV1File),
|
||||||
|
"basePath: /api/v1", "basePath: "+cfg.Web.BasePath+"/api/v1", 1))
|
||||||
|
|
||||||
|
s.root.HandleFunc("GET /doc/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v0_swagger.yaml" {
|
||||||
|
respond.Data(w, http.StatusOK, "application/yaml", customV0File)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Path == s.cfg.Web.BasePath+"/doc/v1_swagger.yaml" {
|
||||||
|
respond.Data(w, http.StatusOK, "application/yaml", customV1File)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Setup routes
|
// Setup routes
|
||||||
s.setupRoutes(endpoints...)
|
s.setupRoutes(endpoints...)
|
||||||
@@ -128,14 +160,14 @@ func (s *Server) Run(ctx context.Context, listenAddress string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
||||||
s.server.HandleFunc("GET /api", s.landingPage)
|
s.root.HandleFunc("GET /api", s.landingPage)
|
||||||
s.versions = make(map[ApiVersion]*routegroup.Bundle)
|
s.versions = make(map[ApiVersion]*routegroup.Bundle)
|
||||||
|
|
||||||
for _, setupFunc := range endpoints {
|
for _, setupFunc := range endpoints {
|
||||||
version, groupSetupFn := setupFunc()
|
version, groupSetupFn := setupFunc()
|
||||||
|
|
||||||
if _, ok := s.versions[version]; !ok {
|
if _, ok := s.versions[version]; !ok {
|
||||||
s.versions[version] = s.server.Mount(fmt.Sprintf("/api/%s", version))
|
s.versions[version] = s.root.Mount(fmt.Sprintf("/api/%s", version))
|
||||||
|
|
||||||
// OpenAPI documentation (via RapiDoc)
|
// OpenAPI documentation (via RapiDoc)
|
||||||
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
|
||||||
@@ -149,16 +181,17 @@ func (s *Server) setupRoutes(endpoints ...ApiEndpointSetupFunc) {
|
|||||||
|
|
||||||
func (s *Server) setupFrontendRoutes() {
|
func (s *Server) setupFrontendRoutes() {
|
||||||
// Serve static files
|
// Serve static files
|
||||||
s.server.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
s.root.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app")
|
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app")
|
||||||
})
|
})
|
||||||
|
|
||||||
s.server.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
s.root.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
respond.Redirect(w, r, http.StatusMovedPermanently, "/app/favicon.ico")
|
respond.Redirect(w, r, http.StatusMovedPermanently, s.cfg.Web.BasePath+"/app/favicon.ico")
|
||||||
})
|
})
|
||||||
|
|
||||||
// If a custom frontend path is configured, serve files from there when it contains content.
|
// If a custom frontend path is configured, serve files from there when it contains content.
|
||||||
// If the directory is empty or missing, populate it with the embedded frontend-dist content first.
|
// If the directory is empty or missing, populate it with the embedded frontend-dist content first.
|
||||||
|
useEmbeddedFrontend := true
|
||||||
if s.cfg.Web.FrontendFilePath != "" {
|
if s.cfg.Web.FrontendFilePath != "" {
|
||||||
if err := os.MkdirAll(s.cfg.Web.FrontendFilePath, 0755); err != nil {
|
if err := os.MkdirAll(s.cfg.Web.FrontendFilePath, 0755); err != nil {
|
||||||
slog.Error("failed to create frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
slog.Error("failed to create frontend base directory", "path", s.cfg.Web.FrontendFilePath, "error", err)
|
||||||
@@ -181,18 +214,47 @@ func (s *Server) setupFrontendRoutes() {
|
|||||||
if ok {
|
if ok {
|
||||||
// serve files from FS
|
// serve files from FS
|
||||||
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
slog.Debug("serving frontend files from custom path", "path", s.cfg.Web.FrontendFilePath)
|
||||||
s.server.HandleFiles("/app", http.Dir(s.cfg.Web.FrontendFilePath))
|
useEmbeddedFrontend = false
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: serve embedded frontend files
|
var fileServer http.Handler
|
||||||
s.server.HandleFiles("/app", http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
if useEmbeddedFrontend {
|
||||||
|
fileServer = http.FileServer(http.FS(fsMust(fs.Sub(frontendStatics, "frontend-dist"))))
|
||||||
|
} else {
|
||||||
|
fileServer = http.FileServer(http.Dir(s.cfg.Web.FrontendFilePath))
|
||||||
|
}
|
||||||
|
fileServer = http.StripPrefix(s.cfg.Web.BasePath+"/app", fileServer)
|
||||||
|
|
||||||
|
// Modify index.html and CSS to include the correct base path.
|
||||||
|
var customIndexFile, customCssFile []byte
|
||||||
|
var customCssFileName string
|
||||||
|
if s.cfg.Web.BasePath != "" {
|
||||||
|
customIndexFile, customCssFile, customCssFileName = s.updateBasePathInFrontend(useEmbeddedFrontend)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.root.HandleFunc("GET /app/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// serve a custom index.html file with the correct base path applied
|
||||||
|
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/" {
|
||||||
|
respond.Data(w, http.StatusOK, "text/html", customIndexFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve a custom CSS file with the correct base path applied
|
||||||
|
if s.cfg.Web.BasePath != "" && r.URL.Path == s.cfg.Web.BasePath+"/app/assets/"+customCssFileName {
|
||||||
|
respond.Data(w, http.StatusOK, "text/css", customCssFile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass all other requests to the file server
|
||||||
|
fileServer.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
||||||
s.tpl.HTML(w, http.StatusOK, "index.gohtml", respond.TplData{
|
s.tpl.HTML(w, http.StatusOK, "index.gohtml", respond.TplData{
|
||||||
|
"BasePath": s.cfg.Web.BasePath,
|
||||||
"Version": internal.Version,
|
"Version": internal.Version,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
})
|
})
|
||||||
@@ -201,14 +263,44 @@ func (s *Server) landingPage(w http.ResponseWriter, _ *http.Request) {
|
|||||||
func (s *Server) rapiDocHandler(version ApiVersion) http.HandlerFunc {
|
func (s *Server) rapiDocHandler(version ApiVersion) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.tpl.HTML(w, http.StatusOK, "rapidoc.gohtml", respond.TplData{
|
s.tpl.HTML(w, http.StatusOK, "rapidoc.gohtml", respond.TplData{
|
||||||
"RapiDocSource": "/js/rapidoc-min.js",
|
"RapiDocSource": s.cfg.Web.BasePath + "/js/rapidoc-min.js",
|
||||||
"ApiSpecUrl": fmt.Sprintf("/doc/%s_swagger.yaml", version),
|
"BasePath": s.cfg.Web.BasePath,
|
||||||
|
"ApiSpecUrl": fmt.Sprintf("%s/doc/%s_swagger.yaml", s.cfg.Web.BasePath, version),
|
||||||
"Version": internal.Version,
|
"Version": internal.Version,
|
||||||
"Year": time.Now().Year(),
|
"Year": time.Now().Year(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) updateBasePathInFrontend(useEmbeddedFrontend bool) ([]byte, []byte, string) {
|
||||||
|
if s.cfg.Web.BasePath == "" {
|
||||||
|
return nil, nil, "" // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
var customIndexFile []byte
|
||||||
|
if useEmbeddedFrontend {
|
||||||
|
customIndexFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "index.html")
|
||||||
|
} else {
|
||||||
|
customIndexFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "index.html"))
|
||||||
|
}
|
||||||
|
newIndexStr := strings.ReplaceAll(string(customIndexFile), "src=\"/", "src=\""+s.cfg.Web.BasePath+"/")
|
||||||
|
newIndexStr = strings.ReplaceAll(newIndexStr, "href=\"/", "href=\""+s.cfg.Web.BasePath+"/")
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`/app/assets/(index-.+.css)`)
|
||||||
|
match := re.FindStringSubmatch(newIndexStr)
|
||||||
|
cssFileName := match[1]
|
||||||
|
|
||||||
|
var customCssFile []byte
|
||||||
|
if useEmbeddedFrontend {
|
||||||
|
customCssFile, _ = fs.ReadFile(fsMust(fs.Sub(frontendStatics, "frontend-dist")), "assets/"+cssFileName)
|
||||||
|
} else {
|
||||||
|
customCssFile, _ = os.ReadFile(filepath.Join(s.cfg.Web.FrontendFilePath, "/assets/", cssFileName))
|
||||||
|
}
|
||||||
|
newCssStr := strings.ReplaceAll(string(customCssFile), "/app/assets/", s.cfg.Web.BasePath+"/app/assets/")
|
||||||
|
|
||||||
|
return []byte(newIndexStr), []byte(newCssStr), cssFileName
|
||||||
|
}
|
||||||
|
|
||||||
func fsMust(f fs.FS, err error) fs.FS {
|
func fsMust(f fs.FS, err error) fs.FS {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package backend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
"github.com/h44z/wg-portal/internal/config"
|
||||||
@@ -118,3 +119,30 @@ func (p PeerService) SendPeerEmail(
|
|||||||
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
||||||
return p.peers.GetPeerStats(ctx, id)
|
return p.peers.GetPeerStats(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p PeerService) BulkDelete(ctx context.Context, ids []domain.PeerIdentifier) error {
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := p.peers.DeletePeer(ctx, id); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete peer %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PeerService) BulkUpdate(ctx context.Context, ids []domain.PeerIdentifier, updateFn func(*domain.Peer)) error {
|
||||||
|
for _, id := range ids {
|
||||||
|
peer, err := p.peers.GetPeer(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get peer %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFn(peer)
|
||||||
|
|
||||||
|
if _, err := p.peers.UpdatePeer(ctx, peer); err != nil {
|
||||||
|
return fmt.Errorf("failed to update peer %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ func (u UserService) DeactivateApi(ctx context.Context, id domain.UserIdentifier
|
|||||||
return u.users.DeactivateApi(ctx, id)
|
return u.users.DeactivateApi(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserService) ChangePassword(ctx context.Context, id domain.UserIdentifier, oldPassword, newPassword string) (*domain.User, error) {
|
func (u UserService) ChangePassword(
|
||||||
|
ctx context.Context,
|
||||||
|
id domain.UserIdentifier,
|
||||||
|
oldPassword, newPassword string,
|
||||||
|
) (*domain.User, error) {
|
||||||
oldPassword = strings.TrimSpace(oldPassword)
|
oldPassword = strings.TrimSpace(oldPassword)
|
||||||
newPassword = strings.TrimSpace(newPassword)
|
newPassword = strings.TrimSpace(newPassword)
|
||||||
|
|
||||||
@@ -121,3 +125,30 @@ func (u UserService) GetUserPeerStats(ctx context.Context, id domain.UserIdentif
|
|||||||
func (u UserService) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
|
func (u UserService) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
|
||||||
return u.wg.GetUserInterfaces(ctx, id)
|
return u.wg.GetUserInterfaces(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u UserService) BulkDelete(ctx context.Context, ids []domain.UserIdentifier) error {
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := u.users.DeleteUser(ctx, id); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UserService) BulkUpdate(ctx context.Context, ids []domain.UserIdentifier, updateFn func(*domain.User)) error {
|
||||||
|
for _, id := range ids {
|
||||||
|
user, err := u.users.GetUser(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFn(user)
|
||||||
|
|
||||||
|
if _, err := u.users.UpdateUser(ctx, user); err != nil {
|
||||||
|
return fmt.Errorf("failed to update user %s: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ func (e ConfigEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
// @Router /config/frontend.js [get]
|
// @Router /config/frontend.js [get]
|
||||||
func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
backendUrl := fmt.Sprintf("%s/api/v0", e.cfg.Web.ExternalUrl)
|
basePath := e.cfg.Web.BasePath
|
||||||
|
backendUrl := fmt.Sprintf("%s%s/api/v0", e.cfg.Web.ExternalUrl, basePath)
|
||||||
if request.Header(r, "x-wg-dev") != "" {
|
if request.Header(r, "x-wg-dev") != "" {
|
||||||
referer := request.Header(r, "Referer")
|
referer := request.Header(r, "Referer")
|
||||||
host := "localhost"
|
host := "localhost"
|
||||||
@@ -76,12 +77,13 @@ func (e ConfigEndpoint) handleConfigJsGet() http.HandlerFunc {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
host, port, _ = net.SplitHostPort(parsedReferer.Host)
|
||||||
}
|
}
|
||||||
backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
|
backendUrl = fmt.Sprintf("http://%s:%s%s/api/v0", host,
|
||||||
port) // override if request comes from frontend started with npm run dev
|
port, basePath) // override if request comes from frontend started with npm run dev
|
||||||
}
|
}
|
||||||
|
|
||||||
e.tpl.Render(w, http.StatusOK, "frontend_config.js.gotpl", "text/javascript", map[string]any{
|
e.tpl.Render(w, http.StatusOK, "frontend_config.js.gotpl", "text/javascript", map[string]any{
|
||||||
"BackendUrl": backendUrl,
|
"BackendUrl": backendUrl,
|
||||||
|
"BasePath": basePath,
|
||||||
"Version": internal.Version,
|
"Version": internal.Version,
|
||||||
"SiteTitle": e.cfg.Web.SiteTitle,
|
"SiteTitle": e.cfg.Web.SiteTitle,
|
||||||
"SiteCompanyName": e.cfg.Web.SiteCompanyName,
|
"SiteCompanyName": e.cfg.Web.SiteCompanyName,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ type PeerService interface {
|
|||||||
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
|
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
|
||||||
// GetPeerStats returns the peer stats for the given interface.
|
// GetPeerStats returns the peer stats for the given interface.
|
||||||
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
||||||
|
// BulkDelete deletes multiple peers.
|
||||||
|
BulkDelete(context.Context, []domain.PeerIdentifier) error
|
||||||
|
// BulkUpdate modifies multiple peers.
|
||||||
|
BulkUpdate(context.Context, []domain.PeerIdentifier, func(*domain.Peer)) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerEndpoint struct {
|
type PeerEndpoint struct {
|
||||||
@@ -84,6 +89,9 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup.HandleFunc("GET /{id}", e.handleSingleGet())
|
apiGroup.HandleFunc("GET /{id}", e.handleSingleGet())
|
||||||
apiGroup.HandleFunc("PUT /{id}", e.handleUpdatePut())
|
apiGroup.HandleFunc("PUT /{id}", e.handleUpdatePut())
|
||||||
apiGroup.HandleFunc("DELETE /{id}", e.handleDelete())
|
apiGroup.HandleFunc("DELETE /{id}", e.handleDelete())
|
||||||
|
apiGroup.HandleFunc("POST /bulk-delete", e.handleBulkDelete())
|
||||||
|
apiGroup.HandleFunc("POST /bulk-enable", e.handleBulkEnable())
|
||||||
|
apiGroup.HandleFunc("POST /bulk-disable", e.handleBulkDisable())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm Handler function.
|
// handleAllGet returns a gorm Handler function.
|
||||||
@@ -521,3 +529,114 @@ func (e PeerEndpoint) getConfigStyle(r *http.Request) string {
|
|||||||
}
|
}
|
||||||
return configStyle
|
return configStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleBulkDelete returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleBulkDelete
|
||||||
|
// @Tags Peer
|
||||||
|
// @Summary Bulk delete selected peers.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to delete"
|
||||||
|
// @Success 204 "No content if deletion was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /peer/bulk-delete [post]
|
||||||
|
func (e PeerEndpoint) handleBulkDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkPeerRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.PeerIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.peerService.BulkDelete(r.Context(), ids)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkEnable returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleBulkEnable
|
||||||
|
// @Tags Peer
|
||||||
|
// @Summary Bulk enable selected peers.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to enable"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /peer/bulk-enable [post]
|
||||||
|
func (e PeerEndpoint) handleBulkEnable() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkPeerRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.PeerIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.peerService.BulkUpdate(r.Context(), ids, func(p *domain.Peer) {
|
||||||
|
p.Disabled = nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkDisable returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleBulkDisable
|
||||||
|
// @Tags Peer
|
||||||
|
// @Summary Bulk disable selected peers.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of peer identifiers to disable"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /peer/bulk-disable [post]
|
||||||
|
func (e PeerEndpoint) handleBulkDisable() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkPeerRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.PeerIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.PeerIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
err := e.peerService.BulkUpdate(r.Context(), ids, func(p *domain.Peer) {
|
||||||
|
p.Disabled = &now
|
||||||
|
p.DisabledReason = domain.DisabledReasonAdmin
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
|
|
||||||
@@ -36,6 +37,10 @@ type UserService interface {
|
|||||||
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
|
||||||
// GetUserInterfaces returns all interfaces for the given user.
|
// GetUserInterfaces returns all interfaces for the given user.
|
||||||
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
|
GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error)
|
||||||
|
// BulkDelete deletes multiple users.
|
||||||
|
BulkDelete(ctx context.Context, ids []domain.UserIdentifier) error
|
||||||
|
// BulkUpdate modifies multiple users.
|
||||||
|
BulkUpdate(ctx context.Context, ids []domain.UserIdentifier, updateFn func(*domain.User)) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserEndpoint struct {
|
type UserEndpoint struct {
|
||||||
@@ -77,7 +82,13 @@ func (e UserEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|||||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("GET /{id}/interfaces", e.handleInterfacesGet())
|
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("GET /{id}/interfaces", e.handleInterfacesGet())
|
||||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/enable", e.handleApiEnablePost())
|
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/enable", e.handleApiEnablePost())
|
||||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/disable", e.handleApiDisablePost())
|
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/api/disable", e.handleApiDisablePost())
|
||||||
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/change-password", e.handleChangePasswordPost())
|
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/change-password",
|
||||||
|
e.handleChangePasswordPost())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-delete", e.handleBulkDelete())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-enable", e.handleBulkEnable())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-disable", e.handleBulkDisable())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-lock", e.handleBulkLock())
|
||||||
|
apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /bulk-unlock", e.handleBulkUnlock())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAllGet returns a gorm Handler function.
|
// handleAllGet returns a gorm Handler function.
|
||||||
@@ -459,3 +470,190 @@ func (e UserEndpoint) handleChangePasswordPost() http.HandlerFunc {
|
|||||||
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
respond.JSON(w, http.StatusOK, model.NewUser(user, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleBulkDelete returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleBulkDelete
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Bulk delete selected users.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of user identifiers to delete"
|
||||||
|
// @Success 204 "No content if deletion was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/bulk-delete [post]
|
||||||
|
func (e UserEndpoint) handleBulkDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkUserRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.UserIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.userService.BulkDelete(r.Context(), ids)
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkEnable returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleBulkEnable
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Bulk enable selected users.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of user identifiers to enable"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/bulk-enable [post]
|
||||||
|
func (e UserEndpoint) handleBulkEnable() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkUserRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.UserIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||||
|
user.Disabled = nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkDisable returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleBulkDisable
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Bulk disable selected users.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of user identifiers to disable"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/bulk-disable [post]
|
||||||
|
func (e UserEndpoint) handleBulkDisable() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkUserRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.UserIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||||
|
user.Disabled = &now
|
||||||
|
user.DisabledReason = domain.DisabledReasonAdmin
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkLock returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleBulkLock
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Bulk lock selected users.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of user identifiers to lock"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/bulk-lock [post]
|
||||||
|
func (e UserEndpoint) handleBulkLock() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkUserRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.UserIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||||
|
user.Locked = &now
|
||||||
|
user.LockedReason = domain.LockedReasonAdmin
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBulkUnlock returns a gorm Handler function.
|
||||||
|
//
|
||||||
|
// @ID users_handleBulkUnlock
|
||||||
|
// @Tags Users
|
||||||
|
// @Summary Bulk unlock selected users.
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body model.BulkPeerRequest true "A list of user identifiers to unlock"
|
||||||
|
// @Success 204 "No content if action was successful"
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /user/bulk-unlock [post]
|
||||||
|
func (e UserEndpoint) handleBulkUnlock() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req model.BulkUserRequest
|
||||||
|
if err := request.BodyJson(r, &req); err != nil {
|
||||||
|
respond.JSON(w, http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]domain.UserIdentifier, len(req.Identifiers))
|
||||||
|
for i, id := range req.Identifiers {
|
||||||
|
ids[i] = domain.UserIdentifier(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.userService.BulkUpdate(r.Context(), ids, func(user *domain.User) {
|
||||||
|
user.Locked = nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
respond.JSON(w, http.StatusInternalServerError,
|
||||||
|
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respond.Status(w, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
WGPORTAL_BACKEND_BASE_URL="{{ $.BackendUrl }}";
|
WGPORTAL_BACKEND_BASE_URL="{{ $.BackendUrl }}";
|
||||||
|
WGPORTAL_BASE_PATH="{{ $.BasePath }}";
|
||||||
WGPORTAL_VERSION="{{ $.Version }}";
|
WGPORTAL_VERSION="{{ $.Version }}";
|
||||||
WGPORTAL_SITE_TITLE="{{ $.SiteTitle }}";
|
WGPORTAL_SITE_TITLE="{{ $.SiteTitle }}";
|
||||||
WGPORTAL_SITE_COMPANY_NAME="{{ $.SiteCompanyName }}";
|
WGPORTAL_SITE_COMPANY_NAME="{{ $.SiteCompanyName }}";
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ func NewSessionWrapper(cfg *config.Config) *SessionWrapper {
|
|||||||
sessionManager.Cookie.Secure = strings.HasPrefix(cfg.Web.ExternalUrl, "https")
|
sessionManager.Cookie.Secure = strings.HasPrefix(cfg.Web.ExternalUrl, "https")
|
||||||
sessionManager.Cookie.HttpOnly = true
|
sessionManager.Cookie.HttpOnly = true
|
||||||
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
|
sessionManager.Cookie.SameSite = http.SameSiteLaxMode
|
||||||
|
if cfg.Web.BasePath != "" {
|
||||||
|
sessionManager.Cookie.Path = cfg.Web.BasePath
|
||||||
|
} else {
|
||||||
sessionManager.Cookie.Path = "/"
|
sessionManager.Cookie.Path = "/"
|
||||||
|
}
|
||||||
sessionManager.Cookie.Persist = false
|
sessionManager.Cookie.Persist = false
|
||||||
|
|
||||||
wrappedSessionManager := &SessionWrapper{sessionManager}
|
wrappedSessionManager := &SessionWrapper{sessionManager}
|
||||||
|
|||||||
10
internal/app/api/v0/model/models_bulk.go
Normal file
10
internal/app/api/v0/model/models_bulk.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type BulkPeerRequest struct {
|
||||||
|
Identifiers []string `json:"Identifiers" binding:"required"`
|
||||||
|
Reason string `json:"Reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BulkUserRequest struct {
|
||||||
|
Identifiers []string `json:"Identifiers" binding:"required"`
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ type Authenticator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthenticator creates a new Authenticator instance.
|
// NewAuthenticator creates a new Authenticator instance.
|
||||||
func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserManager) (
|
func NewAuthenticator(cfg *config.Auth, extUrl, basePath string, bus EventBus, users UserManager) (
|
||||||
*Authenticator,
|
*Authenticator,
|
||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
@@ -107,7 +107,7 @@ func NewAuthenticator(cfg *config.Auth, extUrl string, bus EventBus, users UserM
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
users: users,
|
users: users,
|
||||||
callbackUrlPrefix: fmt.Sprintf("%s/api/v0", extUrl),
|
callbackUrlPrefix: fmt.Sprintf("%s%s/api/v0", extUrl, basePath),
|
||||||
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
oauthAuthenticators: make(map[string]AuthenticatorOauth, len(cfg.OpenIDConnect)+len(cfg.OAuth)),
|
||||||
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
ldapAuthenticators: make(map[string]AuthenticatorLdap, len(cfg.Ldap)),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ func defaultConfig() *Config {
|
|||||||
RequestLogging: getEnvBool("WG_PORTAL_WEB_REQUEST_LOGGING", false),
|
RequestLogging: getEnvBool("WG_PORTAL_WEB_REQUEST_LOGGING", false),
|
||||||
ExposeHostInfo: getEnvBool("WG_PORTAL_WEB_EXPOSE_HOST_INFO", false),
|
ExposeHostInfo: getEnvBool("WG_PORTAL_WEB_EXPOSE_HOST_INFO", false),
|
||||||
ExternalUrl: getEnvStr("WG_PORTAL_WEB_EXTERNAL_URL", "http://localhost:8888"),
|
ExternalUrl: getEnvStr("WG_PORTAL_WEB_EXTERNAL_URL", "http://localhost:8888"),
|
||||||
|
BasePath: getEnvStr("WG_PORTAL_WEB_BASE_PATH", ""),
|
||||||
ListeningAddress: getEnvStr("WG_PORTAL_WEB_LISTENING_ADDRESS", ":8888"),
|
ListeningAddress: getEnvStr("WG_PORTAL_WEB_LISTENING_ADDRESS", ":8888"),
|
||||||
SessionIdentifier: getEnvStr("WG_PORTAL_WEB_SESSION_IDENTIFIER", "wgPortalSession"),
|
SessionIdentifier: getEnvStr("WG_PORTAL_WEB_SESSION_IDENTIFIER", "wgPortalSession"),
|
||||||
SessionSecret: getEnvStr("WG_PORTAL_WEB_SESSION_SECRET", "very_secret"),
|
SessionSecret: getEnvStr("WG_PORTAL_WEB_SESSION_SECRET", "very_secret"),
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ type WebConfig struct {
|
|||||||
// ExternalUrl is the URL where a client can access WireGuard Portal.
|
// ExternalUrl is the URL where a client can access WireGuard Portal.
|
||||||
// This is used for the callback URL of the OAuth providers.
|
// This is used for the callback URL of the OAuth providers.
|
||||||
ExternalUrl string `yaml:"external_url"`
|
ExternalUrl string `yaml:"external_url"`
|
||||||
|
// BasePath is an optional URL path prefix under which the whole web UI and API are served.
|
||||||
|
// Example: "/wg" will make the UI available at /wg/app and the API at /wg/api.
|
||||||
|
// Empty string means no prefix (served from root path).
|
||||||
|
BasePath string `yaml:"base_path"`
|
||||||
// ListeningAddress is the address and port for the web server.
|
// ListeningAddress is the address and port for the web server.
|
||||||
ListeningAddress string `yaml:"listening_address"`
|
ListeningAddress string `yaml:"listening_address"`
|
||||||
// SessionIdentifier is the session identifier for the web frontend.
|
// SessionIdentifier is the session identifier for the web frontend.
|
||||||
@@ -35,4 +39,12 @@ type WebConfig struct {
|
|||||||
|
|
||||||
func (c *WebConfig) Sanitize() {
|
func (c *WebConfig) Sanitize() {
|
||||||
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
c.ExternalUrl = strings.TrimRight(c.ExternalUrl, "/")
|
||||||
|
|
||||||
|
// normalize BasePath: allow empty, otherwise ensure leading slash, no trailing slash
|
||||||
|
p := strings.TrimSpace(c.BasePath)
|
||||||
|
p = strings.TrimRight(p, "/")
|
||||||
|
if p != "" && !strings.HasPrefix(p, "/") {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
c.BasePath = p
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user