Compare commits

..

2 Commits

Author SHA1 Message Date
Christoph Haas
ace05947e8 docs: remove invalid mail templates section from configuration overview 2026-01-05 23:18:38 +01:00
Christoph Haas
d66d6ed3fd docs: enhance binary usage guide and systemd setup (#577) 2026-01-05 23:18:24 +01:00
28 changed files with 31 additions and 1467 deletions

View File

@@ -24,7 +24,7 @@ jobs:
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Get Version
shell: bash
@@ -96,7 +96,7 @@ jobs:
done
- name: Upload binaries
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: binaries
path: binaries/wg-portal_linux*
@@ -110,7 +110,7 @@ jobs:
contents: write
steps:
- name: Download binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: binaries

View File

@@ -47,7 +47,7 @@ func main() {
rawDb, err := adapters.NewDatabase(cfg.Database)
internal.AssertNoError(err)
database, err := adapters.NewSqlRepository(rawDb, cfg)
database, err := adapters.NewSqlRepository(rawDb)
internal.AssertNoError(err)
wireGuard, err := wireguard.NewControllerManager(cfg)

View File

@@ -157,14 +157,12 @@ More advanced options are found in the subsequent `Advanced` section.
### `create_default_peer`
- **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER`
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI).
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
### `create_default_peer_on_creation`
- **Default:** `false`
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION`
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
- **Important:** This option requires [create_default_peer](#create_default_peer) to be enabled.
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
### `re_enable_peer_after_user_enable`
- **Default:** `true`

View File

@@ -83,7 +83,6 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = interfaces.Prepared.Identifier
formData.value.DisplayName = interfaces.Prepared.DisplayName
formData.value.Mode = interfaces.Prepared.Mode
formData.value.CreateDefaultPeer = interfaces.Prepared.CreateDefaultPeer
formData.value.Backend = interfaces.Prepared.Backend
formData.value.PublicKey = interfaces.Prepared.PublicKey
@@ -123,7 +122,6 @@ watch(() => props.visible, async (newValue, oldValue) => {
formData.value.Identifier = selectedInterface.value.Identifier
formData.value.DisplayName = selectedInterface.value.DisplayName
formData.value.Mode = selectedInterface.value.Mode
formData.value.CreateDefaultPeer = selectedInterface.value.CreateDefaultPeer
formData.value.Backend = selectedInterface.value.Backend
formData.value.PublicKey = selectedInterface.value.PublicKey
@@ -489,10 +487,6 @@ async function del() {
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
</div>
<div class="form-check form-switch" v-if="formData.Mode==='server' && settings.Setting('CreateDefaultPeer')">
<input v-model="formData.CreateDefaultPeer" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t('modals.interface-edit.create-default-peer.label') }}</label>
</div>
<div class="form-check form-switch" v-if="formData.Backend==='local'">
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>

View File

@@ -4,7 +4,6 @@ export function freshInterface() {
Disabled: false,
DisplayName: "",
Identifier: "",
CreateDefaultPeer: false,
Mode: "server",
Backend: "local",

View File

@@ -129,11 +129,6 @@
"button-add-peers": "Mehrere Peers hinzufügen",
"button-show-peer": "Peer anzeigen",
"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-expiring": "Peer läuft ab am",
"peer-connected": "Verbunden",
@@ -158,14 +153,6 @@
"button-add-user": "Benutzer hinzufügen",
"button-show-user": "Benutzer anzeigen",
"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-locked": "Konto ist gesperrt, Grund:",
"admin": "Benutzer hat Administratorrechte",
@@ -469,9 +456,6 @@
"disabled": {
"label": "Schnittstelle deaktiviert"
},
"create-default-peer": {
"label": "Peer für neue Benutzer automatisch erstellen"
},
"save-config": {
"label": "wg-quick Konfiguration automatisch speichern"
},

View File

@@ -129,11 +129,6 @@
"button-add-peers": "Add Multiple Peers",
"button-show-peer": "Show 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-expiring": "Peer is expiring at",
"peer-connected": "Connected",
@@ -158,14 +153,6 @@
"button-add-user": "Add User",
"button-show-user": "Show 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-locked": "Account is locked, reason:",
"admin": "User has administrator privileges",
@@ -469,9 +456,6 @@
"disabled": {
"label": "Interface Disabled"
},
"create-default-peer": {
"label": "Create default peer for new users"
},
"save-config": {
"label": "Automatically save wg-quick config"
},

View File

@@ -222,73 +222,6 @@ export const peerStore = defineStore('peers', {
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) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${base64_url_encode(id)}`, formData)

View File

@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
import {apiWrapper} from "@/helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {authStore} from "@/stores/auth";
import {peerStore} from "@/stores/peers";
import { base64_url_encode } from '@/helpers/encoding';
import {freshStats} from "@/helpers/models";
import { ipToBigInt } from '@/helpers/utils';
@@ -219,18 +218,5 @@ 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)
})
},
}
})

View File

@@ -142,140 +142,5 @@ 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)
})
},
}
})

View File

@@ -29,10 +29,6 @@ const sortKey = ref("")
const sortOrder = ref(1)
const selectAll = ref(false)
const selectedPeers = computed(() => {
return peers.All.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
})
function sortBy(key) {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value * -1; // Toggle sort order
@@ -115,39 +111,6 @@ 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() {
peers.FilteredAndPaged.forEach(peer => {
peer.IsSelected = selectAll.value;
@@ -390,13 +353,6 @@ 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>
</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="peers.Count===0">
<h4>{{ $t('interfaces.no-peer.headline') }}</h4>

View File

@@ -1,19 +1,14 @@
<script setup>
import PeerViewModal from "../components/PeerViewModal.vue";
import { onMounted, ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { onMounted, ref } from "vue";
import { profileStore } from "@/stores/profile";
import { peerStore } from "@/stores/peers";
import UserPeerEditModal from "@/components/UserPeerEditModal.vue";
import { settingsStore } from "@/stores/settings";
import { humanFileSize } from "@/helpers/utils";
const settings = settingsStore()
const profile = profileStore()
const peers = peerStore()
const { t } = useI18n()
const viewedPeerId = ref("")
const editPeerId = ref("")
@@ -22,10 +17,6 @@ const sortKey = ref("")
const sortOrder = ref(1)
const selectAll = ref(false)
const selectedPeers = computed(() => {
return profile.Peers.filter(peer => peer.IsSelected).map(peer => peer.Identifier);
})
function sortBy(key) {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value * -1; // Toggle sort order
@@ -44,17 +35,6 @@ function friendlyInterfaceName(id, name) {
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() {
profile.FilteredAndPagedPeers.forEach(peer => {
peer.IsSelected = selectAll.value;
@@ -104,13 +84,6 @@ onMounted(async () => {
</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 v-if="profile.CountPeers === 0">
<h4>{{ $t('profile.no-peer.headline') }}</h4>

View File

@@ -1,77 +1,16 @@
<script setup>
import {userStore} from "@/stores/users";
import {ref, onMounted, computed} from "vue";
import {ref,onMounted} from "vue";
import UserEditModal from "../components/UserEditModal.vue";
import UserViewModal from "../components/UserViewModal.vue";
import {useI18n} from "vue-i18n";
const users = userStore()
const { t } = useI18n()
const editUserId = ref("")
const viewedUserId = ref("")
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() {
users.FilteredAndPaged.forEach(user => {
user.IsSelected = selectAll.value;
@@ -106,15 +45,6 @@ onMounted(() => {
</a>
</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 v-if="users.Count===0">
<h4>{{ $t('users.no-user.headline') }}</h4>

View File

@@ -24,7 +24,7 @@ import (
)
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
var SchemaVersion uint64 = 2
var SchemaVersion uint64 = 1
// SysStat stores the current database schema version and the timestamp when it was applied.
type SysStat struct {
@@ -179,15 +179,13 @@ func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
// SqlRepo is a SQL database repository implementation.
// Currently, it supports MySQL, SQLite, Microsoft SQL and Postgresql database systems.
type SqlRepo struct {
db *gorm.DB
cfg *config.Config
db *gorm.DB
}
// NewSqlRepository creates a new SqlRepo instance.
func NewSqlRepository(db *gorm.DB, cfg *config.Config) (*SqlRepo, error) {
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
repo := &SqlRepo{
db: db,
cfg: cfg,
db: db,
}
if err := repo.preCheck(); err != nil {
@@ -234,9 +232,7 @@ func (r *SqlRepo) migrate() error {
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
existingSysStat := SysStat{}
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
// Migration: 0 --> 1
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
if existingSysStat.SchemaVersion == 0 {
sysStat := SysStat{
MigratedAt: time.Now(),
@@ -248,27 +244,6 @@ func (r *SqlRepo) migrate() error {
slog.Debug("sys-stat entry written", "schema_version", SchemaVersion)
}
// Migration: 1 --> 2
if existingSysStat.SchemaVersion == 1 {
// Preserve existing behavior for installations that had default-peer-creation enabled.
if r.cfg.Core.CreateDefaultPeer {
err := r.db.Model(&domain.Interface{}).
Where("type = ?", domain.InterfaceTypeServer).
Update("create_default_peer", true).Error
if err != nil {
return fmt.Errorf("failed to migrate interface flags for schema version %d: %w", SchemaVersion, err)
}
slog.Debug("migrated interface create_default_peer flags", "schema_version", SchemaVersion)
}
sysStat := SysStat{
MigratedAt: time.Now(),
SchemaVersion: SchemaVersion,
}
if err := r.db.Create(&sysStat).Error; err != nil {
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
}
}
return nil
}

View File

@@ -800,126 +800,6 @@
}
}
},
"/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": {
"post": {
"produces": [
@@ -1444,206 +1324,6 @@
}
}
},
"/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": {
"post": {
"produces": [
@@ -2057,23 +1737,6 @@
}
}
},
"model.BulkPeerRequest": {
"type": "object",
"required": [
"Identifiers"
],
"properties": {
"Identifiers": {
"type": "array",
"items": {
"type": "string"
}
},
"Reason": {
"type": "string"
}
}
},
"model.ConfigOption-array_string": {
"type": "object",
"properties": {

View File

@@ -16,17 +16,6 @@ definitions:
Timestamp:
type: string
type: object
model.BulkPeerRequest:
properties:
Identifiers:
items:
type: string
type: array
Reason:
type: string
required:
- Identifiers
type: object
model.ConfigOption-array_string:
properties:
Overridable:
@@ -1091,84 +1080,6 @@ paths:
summary: Update the given peer record.
tags:
- 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:
post:
operationId: peers_handleEmailPost
@@ -1660,136 +1571,6 @@ paths:
summary: Get all user records.
tags:
- 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:
post:
operationId: users_handleCreatePost

View File

@@ -2,7 +2,6 @@ package backend
import (
"context"
"fmt"
"io"
"github.com/h44z/wg-portal/internal/config"
@@ -119,30 +118,3 @@ func (p PeerService) SendPeerEmail(
func (p PeerService) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
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
}

View File

@@ -72,11 +72,7 @@ func (u UserService) DeactivateApi(ctx context.Context, id domain.UserIdentifier
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)
newPassword = strings.TrimSpace(newPassword)
@@ -125,30 +121,3 @@ func (u UserService) GetUserPeerStats(ctx context.Context, id domain.UserIdentif
func (u UserService) GetUserInterfaces(ctx context.Context, id domain.UserIdentifier) ([]domain.Interface, error) {
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
}

View File

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

View File

@@ -4,7 +4,6 @@ import (
"context"
"io"
"net/http"
"time"
"github.com/go-pkgz/routegroup"
@@ -42,10 +41,6 @@ type PeerService interface {
SendPeerEmail(ctx context.Context, linkOnly bool, style string, peers ...domain.PeerIdentifier) error
// GetPeerStats returns the peer stats for the given interface.
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 {
@@ -89,9 +84,6 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) {
apiGroup.HandleFunc("GET /{id}", e.handleSingleGet())
apiGroup.HandleFunc("PUT /{id}", e.handleUpdatePut())
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.
@@ -529,114 +521,3 @@ func (e PeerEndpoint) getConfigStyle(r *http.Request) string {
}
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)
}
}

View File

@@ -3,7 +3,6 @@ package handlers
import (
"context"
"net/http"
"time"
"github.com/go-pkgz/routegroup"
@@ -37,10 +36,6 @@ type UserService interface {
GetUserPeerStats(ctx context.Context, id domain.UserIdentifier) ([]domain.PeerStatus, error)
// GetUserInterfaces returns all interfaces for the given user.
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 {
@@ -82,13 +77,7 @@ 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("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}/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())
apiGroup.With(e.authenticator.UserIdMatch("id")).HandleFunc("POST /{id}/change-password", e.handleChangePasswordPost())
}
// handleAllGet returns a gorm Handler function.
@@ -470,190 +459,3 @@ func (e UserEndpoint) handleChangePasswordPost() http.HandlerFunc {
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)
}
}

View File

@@ -14,7 +14,6 @@ type Settings struct {
MinPasswordLength int `json:"MinPasswordLength"`
AvailableBackends []SettingsBackendNames `json:"AvailableBackends"`
LoginFormVisible bool `json:"LoginFormVisible"`
CreateDefaultPeer bool `json:"CreateDefaultPeer"`
}
type SettingsBackendNames struct {

View File

@@ -1,10 +0,0 @@
package model
type BulkPeerRequest struct {
Identifiers []string `json:"Identifiers" binding:"required"`
Reason string `json:"Reason"`
}
type BulkUserRequest struct {
Identifiers []string `json:"Identifiers" binding:"required"`
}

View File

@@ -9,16 +9,15 @@ import (
)
type Interface struct {
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
ListenPort int `json:"ListenPort"` // the listening port, for example: 51820
Addresses []string `json:"Addresses"` // the interface ip addresses
@@ -66,7 +65,6 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason,
SaveConfig: src.SaveConfig,
CreateDefaultPeer: src.CreateDefaultPeer,
ListenPort: src.ListenPort,
Addresses: domain.CidrsToStringSlice(src.Addresses),
Dns: internal.SliceString(src.DnsStr),
@@ -153,7 +151,6 @@ func NewDomainInterface(src *Interface) *domain.Interface {
PreDown: src.PreDown,
PostDown: src.PostDown,
SaveConfig: src.SaveConfig,
CreateDefaultPeer: src.CreateDefaultPeer,
DisplayName: src.DisplayName,
Type: domain.InterfaceType(src.Mode),
Backend: domain.InterfaceBackend(src.Backend),

View File

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

View File

@@ -35,10 +35,6 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
continue // only create default peers for server interfaces
}
if !iface.CreateDefaultPeer {
continue // only create default peers if the interface flag is set
}
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
return peer.InterfaceIdentifier == iface.Identifier
})

View File

@@ -78,12 +78,7 @@ func (f *mockDB) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceId
func (f *mockDB) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
return nil, nil
}
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
if f.iface != nil {
return []domain.Interface{*f.iface}, nil
}
return nil, nil
}
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { return nil, nil }
func (f *mockDB) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
return nil, nil
}
@@ -197,58 +192,3 @@ func TestCreatePeer_SetsIdentifier_FromPublicKey(t *testing.T) {
t.Fatalf("expected peer with identifier %q to be saved in DB", expectedId)
}
}
func TestCreateDefaultPeer_RespectsInterfaceFlag(t *testing.T) {
// Arrange
cfg := &config.Config{}
cfg.Core.CreateDefaultPeer = true
bus := &mockBus{}
ctrlMgr := &ControllerManager{
controllers: map[domain.InterfaceBackend]backendInstance{
config.LocalBackendName: {Implementation: &mockController{}},
},
}
db := &mockDB{
iface: &domain.Interface{
Identifier: "wg0",
Type: domain.InterfaceTypeServer,
CreateDefaultPeer: false, // Flag is disabled!
},
}
m := Manager{
cfg: cfg,
bus: bus,
db: db,
wg: ctrlMgr,
}
userId := domain.UserIdentifier("user@example.com")
ctx := domain.SetUserInfo(context.Background(), &domain.ContextUserInfo{Id: userId, IsAdmin: true})
// Act
err := m.CreateDefaultPeer(ctx, userId)
// Assert
if err != nil {
t.Fatalf("CreateDefaultPeer returned error: %v", err)
}
if len(db.savedPeers) != 0 {
t.Fatalf("expected no peers to be created because interface flag is false, but got %d", len(db.savedPeers))
}
// Now enable the flag and try again
db.iface.CreateDefaultPeer = true
err = m.CreateDefaultPeer(ctx, userId)
if err != nil {
t.Fatalf("CreateDefaultPeer returned error after enabling flag: %v", err)
}
if len(db.savedPeers) != 1 {
t.Fatalf("expected 1 peer to be created because interface flag is true, but got %d", len(db.savedPeers))
}
}

View File

@@ -53,13 +53,12 @@ type Interface struct {
SaveConfig bool // automatically persist config changes to the wgX.conf file
// WG Portal specific
DisplayName string // a nice display name/ description for the interface
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
CreateDefaultPeer bool // if true, default peers will be created for this interface
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
DriverType string // the interface driver type (linux, software, ...)
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string // the reason why the interface has been disabled
DisplayName string // a nice display name/ description for the interface
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
DriverType string // the interface driver type (linux, software, ...)
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
DisabledReason string // the reason why the interface has been disabled
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
// the peer config