From 33dcc800782829274605ab50deb64bb08c6824f1 Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Sun, 18 May 2025 19:49:31 +0200 Subject: [PATCH] prepare frontend for different WireGuard backends (#426) --- .../src/components/InterfaceEditModal.vue | 30 ++++++++---- frontend/src/helpers/models.js | 1 + frontend/src/lang/translations/de.json | 2 +- frontend/src/lang/translations/en.json | 6 ++- frontend/src/lang/translations/fr.json | 2 +- frontend/src/lang/translations/ko.json | 2 +- frontend/src/lang/translations/pt.json | 2 +- frontend/src/lang/translations/ru.json | 2 +- frontend/src/lang/translations/uk.json | 2 +- frontend/src/lang/translations/vi.json | 2 +- frontend/src/lang/translations/zh.json | 2 +- frontend/src/views/InterfaceView.vue | 21 +++++++- .../app/api/v0/handlers/endpoint_config.go | 22 ++++++++- internal/app/api/v0/model/models.go | 18 ++++--- internal/app/api/v0/model/models_interface.go | 3 ++ internal/config/backend.go | 48 +++++++++++++++++++ internal/config/config.go | 16 +++++++ 17 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 internal/config/backend.go diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue index d23b490..74e7e4e 100644 --- a/frontend/src/components/InterfaceEditModal.vue +++ b/frontend/src/components/InterfaceEditModal.vue @@ -10,11 +10,13 @@ import isCidr from "is-cidr"; import {isIP} from 'is-ip'; import { freshInterface } from '@/helpers/models'; import {peerStore} from "@/stores/peers"; +import {settingsStore} from "@/stores/settings"; const { t } = useI18n() const interfaces = interfaceStore() const peers = peerStore() +const settings = settingsStore() const props = defineProps({ interfaceId: String, @@ -314,13 +316,21 @@ async function del() { -
- - +
+
+ + +
+
+ + +
@@ -385,12 +395,14 @@ async function del() {
-
+
+
+
-
+
diff --git a/frontend/src/helpers/models.js b/frontend/src/helpers/models.js index 8f8683e..6e1e52b 100644 --- a/frontend/src/helpers/models.js +++ b/frontend/src/helpers/models.js @@ -5,6 +5,7 @@ export function freshInterface() { DisplayName: "", Identifier: "", Mode: "server", + Backend: "local", PublicKey: "", PrivateKey: "", diff --git a/frontend/src/lang/translations/de.json b/frontend/src/lang/translations/de.json index ff8af74..6008161 100644 --- a/frontend/src/lang/translations/de.json +++ b/frontend/src/lang/translations/de.json @@ -102,7 +102,7 @@ }, "interface": { "headline": "Schnittstellenstatus für", - "mode": "Modus", + "backend": "Backend", "key": "Öffentlicher Schlüssel", "endpoint": "Öffentlicher Endpunkt", "port": "Port", diff --git a/frontend/src/lang/translations/en.json b/frontend/src/lang/translations/en.json index 06e95e0..d410661 100644 --- a/frontend/src/lang/translations/en.json +++ b/frontend/src/lang/translations/en.json @@ -102,7 +102,7 @@ }, "interface": { "headline": "Interface status for", - "mode": "mode", + "backend": "Backend", "key": "Public Key", "endpoint": "Public Endpoint", "port": "Listening Port", @@ -357,6 +357,10 @@ "client": "Client Mode", "any": "Unknown Mode" }, + "backend": { + "label": "Interface Backend", + "local": "Local WireGuard Backend" + }, "display-name": { "label": "Display Name", "placeholder": "The descriptive name for the interface" diff --git a/frontend/src/lang/translations/fr.json b/frontend/src/lang/translations/fr.json index f5b165c..951ab22 100644 --- a/frontend/src/lang/translations/fr.json +++ b/frontend/src/lang/translations/fr.json @@ -99,7 +99,7 @@ }, "interface": { "headline": "État de l'interface pour", - "mode": "mode", + "backend": "backend", "key": "Clé publique", "endpoint": "Point de terminaison public", "port": "Port d'écoute", diff --git a/frontend/src/lang/translations/ko.json b/frontend/src/lang/translations/ko.json index 8b87d1b..6e65e06 100644 --- a/frontend/src/lang/translations/ko.json +++ b/frontend/src/lang/translations/ko.json @@ -100,7 +100,7 @@ }, "interface": { "headline": "인터페이스 상태:", - "mode": "모드", + "backend": "백엔드", "key": "공개 키", "endpoint": "공개 엔드포인트", "port": "수신 포트", diff --git a/frontend/src/lang/translations/pt.json b/frontend/src/lang/translations/pt.json index a895400..126037e 100644 --- a/frontend/src/lang/translations/pt.json +++ b/frontend/src/lang/translations/pt.json @@ -101,7 +101,7 @@ }, "interface": { "headline": "Status da interface para", - "mode": "modo", + "mode": "backend", "key": "Chave Pública", "endpoint": "Endpoint Público", "port": "Porta de Escuta", diff --git a/frontend/src/lang/translations/ru.json b/frontend/src/lang/translations/ru.json index 6df8383..a88158a 100644 --- a/frontend/src/lang/translations/ru.json +++ b/frontend/src/lang/translations/ru.json @@ -99,7 +99,7 @@ }, "interface": { "headline": "Статус интерфейса для", - "mode": "режим", + "backend": "бэкэнд", "key": "Публичный ключ", "endpoint": "Публичная конечная точка", "port": "Порт прослушивания", diff --git a/frontend/src/lang/translations/uk.json b/frontend/src/lang/translations/uk.json index 7647528..4574ad4 100644 --- a/frontend/src/lang/translations/uk.json +++ b/frontend/src/lang/translations/uk.json @@ -99,7 +99,7 @@ }, "interface": { "headline": "Статус інтерфейсу для", - "mode": "режим", + "backend": "бекенд", "key": "Публічний ключ", "endpoint": "Публічна кінцева точка", "port": "Порт прослуховування", diff --git a/frontend/src/lang/translations/vi.json b/frontend/src/lang/translations/vi.json index 7e90dd9..722918f 100644 --- a/frontend/src/lang/translations/vi.json +++ b/frontend/src/lang/translations/vi.json @@ -98,7 +98,7 @@ }, "interface": { "headline": "Trạng thái giao diện cho", - "mode": "chế độ", + "backend": "phần sau", "key": "Khóa Công khai", "endpoint": "Điểm cuối Công khai", "port": "Cổng Nghe", diff --git a/frontend/src/lang/translations/zh.json b/frontend/src/lang/translations/zh.json index cf1d715..3b5b64e 100644 --- a/frontend/src/lang/translations/zh.json +++ b/frontend/src/lang/translations/zh.json @@ -98,7 +98,7 @@ }, "interface": { "headline": "接口状态", - "mode": "模式", + "backend": "后端", "key": "公钥", "endpoint": "公开节点", "port": "监听端口", diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue index 1084081..b45443b 100644 --- a/frontend/src/views/InterfaceView.vue +++ b/frontend/src/views/InterfaceView.vue @@ -5,17 +5,20 @@ import PeerMultiCreateModal from "../components/PeerMultiCreateModal.vue"; import InterfaceEditModal from "../components/InterfaceEditModal.vue"; import InterfaceViewModal from "../components/InterfaceViewModal.vue"; -import {onMounted, ref} from "vue"; +import {computed, onMounted, ref} from "vue"; import {peerStore} from "@/stores/peers"; import {interfaceStore} from "@/stores/interfaces"; import {notify} from "@kyvg/vue3-notification"; import {settingsStore} from "@/stores/settings"; import {humanFileSize} from '@/helpers/utils'; +import {useI18n} from "vue-i18n"; const settings = settingsStore() const interfaces = interfaceStore() const peers = peerStore() +const { t } = useI18n() + const viewedPeerId = ref("") const editPeerId = ref("") const multiCreatePeerId = ref("") @@ -45,6 +48,20 @@ function calculateInterfaceName(id, name) { return result } +const calculateBackendName = computed(() => { + let backendId = interfaces.GetSelected.Backend + + let backendName = "Unknown" + let availableBackends = settings.Setting('AvailableBackends') || [] + availableBackends.forEach(backend => { + if (backend.Id === backendId) { + backendName = backend.Id === 'local' ? t(backend.Name) : backend.Name + } + }) + return backendName +}) + + async function download() { await interfaces.LoadInterfaceConfig(interfaces.GetSelected.Identifier) @@ -141,7 +158,7 @@ onMounted(async () => {
- {{ $t('interfaces.interface.headline') }} {{interfaces.GetSelected.Identifier}} ({{interfaces.GetSelected.Mode}} {{ $t('interfaces.interface.mode') }}) + {{ $t('interfaces.interface.headline') }} {{interfaces.GetSelected.Identifier}} ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }})
diff --git a/internal/app/api/v0/handlers/endpoint_config.go b/internal/app/api/v0/handlers/endpoint_config.go index a99effe..30bdf51 100644 --- a/internal/app/api/v0/handlers/endpoint_config.go +++ b/internal/app/api/v0/handlers/endpoint_config.go @@ -96,10 +96,29 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionUser := domain.GetUserInfo(r.Context()) + nameFn := func(backend config.Backend) []model.SettingsBackendNames { + names := make([]model.SettingsBackendNames, 0, len(backend.Mikrotik)+1) + + names = append(names, model.SettingsBackendNames{ + Id: backend.Default, + Name: "modals.interface-edit.backend.local", + }) + for _, b := range backend.Mikrotik { + names = append(names, model.SettingsBackendNames{ + Id: b.Id, + Name: b.DisplayName, + }) + } + + return names + + } + // For anonymous users, we return the settings object with minimal information if sessionUser.Id == domain.CtxUnknownUserId || sessionUser.Id == "" { respond.JSON(w, http.StatusOK, model.Settings{ - WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled, + WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled, + AvailableBackends: []model.SettingsBackendNames{}, // return an empty list instead of null }) } else { respond.JSON(w, http.StatusOK, model.Settings{ @@ -109,6 +128,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc { ApiAdminOnly: e.cfg.Advanced.ApiAdminOnly, WebAuthnEnabled: e.cfg.Auth.WebAuthn.Enabled, MinPasswordLength: e.cfg.Auth.MinPasswordLength, + AvailableBackends: nameFn(e.cfg.Backend), }) } } diff --git a/internal/app/api/v0/model/models.go b/internal/app/api/v0/model/models.go index 847b139..9ab0944 100644 --- a/internal/app/api/v0/model/models.go +++ b/internal/app/api/v0/model/models.go @@ -6,10 +6,16 @@ type Error struct { } type Settings struct { - MailLinkOnly bool `json:"MailLinkOnly"` - PersistentConfigSupported bool `json:"PersistentConfigSupported"` - SelfProvisioning bool `json:"SelfProvisioning"` - ApiAdminOnly bool `json:"ApiAdminOnly"` - WebAuthnEnabled bool `json:"WebAuthnEnabled"` - MinPasswordLength int `json:"MinPasswordLength"` + MailLinkOnly bool `json:"MailLinkOnly"` + PersistentConfigSupported bool `json:"PersistentConfigSupported"` + SelfProvisioning bool `json:"SelfProvisioning"` + ApiAdminOnly bool `json:"ApiAdminOnly"` + WebAuthnEnabled bool `json:"WebAuthnEnabled"` + MinPasswordLength int `json:"MinPasswordLength"` + AvailableBackends []SettingsBackendNames `json:"AvailableBackends"` +} + +type SettingsBackendNames struct { + Id string `json:"Id"` + Name string `json:"Name"` } diff --git a/internal/app/api/v0/model/models_interface.go b/internal/app/api/v0/model/models_interface.go index 5684178..a5c3600 100644 --- a/internal/app/api/v0/model/models_interface.go +++ b/internal/app/api/v0/model/models_interface.go @@ -4,6 +4,7 @@ import ( "time" "github.com/h44z/wg-portal/internal" + "github.com/h44z/wg-portal/internal/config" "github.com/h44z/wg-portal/internal/domain" ) @@ -11,6 +12,7 @@ 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) @@ -57,6 +59,7 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface { Identifier: string(src.Identifier), DisplayName: src.DisplayName, Mode: string(src.Type), + Backend: config.LocalBackendName, // TODO: add backend support PrivateKey: src.PrivateKey, PublicKey: src.PublicKey, Disabled: src.IsDisabled(), diff --git a/internal/config/backend.go b/internal/config/backend.go new file mode 100644 index 0000000..dc55b0f --- /dev/null +++ b/internal/config/backend.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" +) + +const LocalBackendName = "local" + +type Backend struct { + Default string `yaml:"default"` // The default backend to use (defaults to the internal backend) + + Mikrotik []BackendMikrotik `yaml:"mikrotik"` +} + +// Validate checks the backend configuration for errors. +func (b *Backend) Validate() error { + if b.Default == "" { + b.Default = LocalBackendName + } + + uniqueMap := make(map[string]struct{}) + for _, backend := range b.Mikrotik { + if backend.Id == LocalBackendName { + return fmt.Errorf("backend ID %q is a reserved keyword", LocalBackendName) + } + if _, exists := uniqueMap[backend.Id]; exists { + return fmt.Errorf("backend ID %q is not unique", backend.Id) + } + uniqueMap[backend.Id] = struct{}{} + } + + if b.Default != LocalBackendName { + if _, ok := uniqueMap[b.Default]; !ok { + return fmt.Errorf("default backend %q is not defined in the configuration", b.Default) + } + } + + return nil +} + +type BackendMikrotik struct { + Id string `yaml:"id"` // A unique id for the Mikrotik backend + DisplayName string `yaml:"display_name"` // A display name for the Mikrotik backend + + ApiUrl string `yaml:"api_url"` // The base URL of the Mikrotik API (e.g., "https://10.10.10.10:8729/rest") + ApiUser string `yaml:"api_user"` + ApiPassword string `yaml:"api_password"` +} diff --git a/internal/config/config.go b/internal/config/config.go index f8ade2f..dd18dbf 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -43,6 +43,8 @@ type Config struct { ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API } `yaml:"advanced"` + Backend Backend `yaml:"backend"` + Statistics struct { UsePingChecks bool `yaml:"use_ping_checks"` PingCheckWorkers int `yaml:"ping_check_workers"` @@ -94,6 +96,12 @@ func (c *Config) LogStartupValues() { "oauthProviders", len(c.Auth.OAuth), "ldapProviders", len(c.Auth.Ldap), ) + + slog.Debug("Config Backend", + "defaultBackend", c.Backend.Default, + "extraBackends", len(c.Backend.Mikrotik), + ) + } // defaultConfig returns the default configuration @@ -117,6 +125,10 @@ func defaultConfig() *Config { DSN: "data/sqlite.db", } + cfg.Backend = Backend{ + Default: LocalBackendName, // local backend is the default (using wgcrtl) + } + cfg.Web = WebConfig{ RequestLogging: false, ExternalUrl: "http://localhost:8888", @@ -194,6 +206,10 @@ func GetConfig() (*Config, error) { } cfg.Web.Sanitize() + err := cfg.Backend.Validate() + if err != nil { + return nil, err + } return cfg, nil }