Remove legacy default-server page, unify on multi-server registry

Deletes /wg-server (route, handlers, template) entirely and adds generic
/servers/:id/interface and /servers/:id/keypair endpoints + UI in the
All Servers page, so every server (including the default one) is managed
through the same per-server registry. Drops the write-through dual-write
hacks that kept the old single-server collection in sync - the registry
is now the single source of truth. One-time legacy-install migration path
is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-07-12 16:27:17 +02:00
co-authored by Claude Sonnet 5
parent 2212141ba3
commit 58db6839c3
10 changed files with 367 additions and 400 deletions
+140
View File
@@ -1453,3 +1453,143 @@ Keine Commits in dieser Session.
- templates/global_settings.html | 93 ++++++++++++++++++++++++++++++++++++++++++ - templates/global_settings.html | 93 ++++++++++++++++++++++++++++++++++++++++++
--- ---
## 2026-07-12 12:54 13:03 (9m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
- 2212141 Clarify server nav labels (Default Server Interface vs All Servers)
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:13 13:24 (11m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:25 13:25 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:26 13:27 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:29 13:29 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:30 13:30 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:33 13:35 (1m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 13:38 13:38 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 +-
- templates/server.html | 6 +--
- templates/servers.html | 4 +-
---
## 2026-07-12 16:20 16:20 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 ++--
- templates/server.html | 6 +++---
- templates/servers.html | 4 ++--
---
## 2026-07-12 16:20 16:21 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** wireguard-ui-multi
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- templates/base.html | 4 ++--
- templates/server.html | 6 +++---
- templates/servers.html | 4 ++--
---
+36 -72
View File
@@ -35,8 +35,8 @@ var usernameRegexp = regexp.MustCompile("^\\w[\\w\\-.]*$")
// resolveServerID returns the server ID a request targets: the :id route // resolveServerID returns the server ID a request targets: the :id route
// param for /servers/:id/... routes, or util.DefaultServerID for the // param for /servers/:id/... routes, or util.DefaultServerID for the
// legacy bare routes (/, /new-client, /wg-server, ...) which always // legacy bare routes (/, /new-client, ...) which always operate on the
// operate on the migrated default server. // migrated default server.
func resolveServerID(c echo.Context) string { func resolveServerID(c echo.Context) string {
if id := c.Param("id"); id != "" { if id := c.Param("id"); id != "" {
return id return id
@@ -1168,27 +1168,22 @@ func RemoveClient(db store.IStore) echo.HandlerFunc {
} }
} }
// WireGuardServer handler // UpdateServerInterfaceHandler updates the WireGuard interface settings
func WireGuardServer(db store.IStore) echo.HandlerFunc { // (addresses, listen port, up/down scripts) for a single server, identified
// by :id. This is the multi-server-aware handler surfaced from the "All
// Servers" page; the per-server registry (servers/<id>.json) is the single
// source of truth for every server, including the default one.
func UpdateServerInterfaceHandler(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
server, err := db.GetServer() serverID := c.Param("id")
if err != nil { if _, err := db.GetServerByID(serverID); err != nil {
log.Error("Cannot get server config: ", err) return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Server not found"})
} }
return c.Render(http.StatusOK, "server.html", map[string]interface{}{
"baseData": model.BaseData{Active: "wg-server", CurrentUser: currentUser(c), Admin: isAdmin(c)},
"serverInterface": server.Interface,
"serverKeyPair": server.KeyPair,
})
}
}
// WireGuardServerInterfaces handler
func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
var serverInterface model.ServerInterface var serverInterface model.ServerInterface
c.Bind(&serverInterface) if err := c.Bind(&serverInterface); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
// validate the input addresses // validate the input addresses
if util.ValidateServerAddresses(serverInterface.Addresses) == false { if util.ValidateServerAddresses(serverInterface.Addresses) == false {
@@ -1198,31 +1193,27 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc {
serverInterface.UpdatedAt = time.Now().UTC() serverInterface.UpdatedAt = time.Now().UTC()
// write config to the database (legacy single-server collection) if err := db.UpdateServerInterface(serverID, serverInterface); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot update server interface: %v", err)})
if err := db.SaveServerInterface(serverInterface); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Interface IP address must be in CIDR format"})
} }
// keep the new per-server registry record for the default server in log.Infof("Updated wireguard server interface settings for server %s: %v", serverID, serverInterface)
// sync, since this legacy route is the only way to edit it today
if serverInterface.Name == "" {
serverInterface.Name = util.DefaultServerID
}
if err := db.UpdateServerInterface(util.DefaultServerID, serverInterface); err != nil {
log.Warnf("Could not sync default server registry entry: %v", err)
}
log.Infof("Updated wireguard server interfaces settings: %v", serverInterface)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated interface addresses successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated interface addresses successfully"})
} }
} }
// WireGuardServerKeyPair handler to generate private and public keys // UpdateServerKeyPairHandler generates a fresh WireGuard key pair for a
func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc { // single server, identified by :id. The per-server registry
// (servers/<id>.json) is the single source of truth for every server,
// including the default one.
func UpdateServerKeyPairHandler(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
// gen Wireguard key pair serverID := c.Param("id")
if _, err := db.GetServerByID(serverID); err != nil {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Server not found"})
}
key, err := wgtypes.GeneratePrivateKey() key, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
log.Error("Cannot generate wireguard key pair: ", err) log.Error("Cannot generate wireguard key pair: ", err)
@@ -1234,17 +1225,11 @@ func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc {
serverKeyPair.PublicKey = key.PublicKey().String() serverKeyPair.PublicKey = key.PublicKey().String()
serverKeyPair.UpdatedAt = time.Now().UTC() serverKeyPair.UpdatedAt = time.Now().UTC()
if err := db.SaveServerKeyPair(serverKeyPair); err != nil { if err := db.UpdateServerKeyPair(serverID, serverKeyPair); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot update server key pair: %v", err)})
} }
// keep the new per-server registry record for the default server in log.Infof("Updated wireguard server key pair for server %s", serverID)
// sync, since this legacy route is the only way to edit it today
if err := db.UpdateServerKeyPair(util.DefaultServerID, serverKeyPair); err != nil {
log.Warnf("Could not sync default server registry entry: %v", err)
}
log.Infof("Updated wireguard server interfaces settings: %v", serverKeyPair)
return c.JSON(http.StatusOK, serverKeyPair) return c.JSON(http.StatusOK, serverKeyPair)
} }
@@ -1379,26 +1364,14 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc {
globalSettings.UpdatedAt = time.Now().UTC() globalSettings.UpdatedAt = time.Now().UTC()
// write config to the database (legacy global collection) // write the app-wide settings (DNS/MTU/PersistentKeepalive); per-server
// concerns (EndpointAddress/Table/FirewallMark/ConfigFilePath) live
// solely in the per-server registry's ServerSetting record, edited via
// the per-server settings editor, and are not duplicated here.
if err := db.SaveGlobalSettings(globalSettings); err != nil { if err := db.SaveGlobalSettings(globalSettings); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"})
} }
// keep the new per-server registry's ServerSetting record for the
// default server in sync (EndpointAddress/Table/FirewallMark/
// ConfigFilePath are per-server concerns in the new model), since
// this legacy route is the only way to edit it today
serverSettings := model.ServerSetting{
EndpointAddress: globalSettings.EndpointAddress,
FirewallMark: globalSettings.FirewallMark,
Table: globalSettings.Table,
ConfigFilePath: globalSettings.ConfigFilePath,
UpdatedAt: globalSettings.UpdatedAt,
}
if err := db.SaveServerSettings(util.DefaultServerID, serverSettings); err != nil {
log.Warnf("Could not sync default server settings registry entry: %v", err)
}
log.Infof("Updated global settings: %v", globalSettings) log.Infof("Updated global settings: %v", globalSettings)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated global settings successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated global settings successfully"})
@@ -1438,7 +1411,7 @@ func GetOrderedSubnetRanges() echo.HandlerFunc {
// SuggestIPAllocation handler to get the list of ip address for client // SuggestIPAllocation handler to get the list of ip address for client
func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
server, err := db.GetServer() server, err := db.GetServerByID(util.DefaultServerID)
if err != nil { if err != nil {
log.Error("Cannot fetch server config from database: ", err) log.Error("Cannot fetch server config from database: ", err)
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, err.Error()}) return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, err.Error()})
@@ -1506,16 +1479,7 @@ func ApplyServerConfig(db store.IStore, tmplDir fs.FS) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
serverID := resolveServerID(c) serverID := resolveServerID(c)
var server model.Server server, err := db.GetServerByID(serverID)
var err error
if serverID == util.DefaultServerID {
// legacy bare route: keep reading from the legacy single-server
// collection, which is still the source of truth an operator
// may have just edited via /wg-server
server, err = db.GetServer()
} else {
server, err = db.GetServerByID(serverID)
}
if err != nil { if err != nil {
log.Error("Cannot get server config: ", err) log.Error("Cannot get server config: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get server config"}) return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get server config"})
+3 -4
View File
@@ -245,9 +245,6 @@ func main() {
app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/client/set-status", handler.SetClientStatus(db), handler.ValidSession, handler.ContentTypeJson)
app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson) app.POST(util.BasePath+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession) app.GET(util.BasePath+"/download", handler.DownloadClient(db), handler.ValidSession)
app.GET(util.BasePath+"/wg-server", handler.WireGuardServer(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/interfaces", handler.WireGuardServerInterfaces(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin)
app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession)
@@ -265,6 +262,8 @@ func main() {
app.POST(util.BasePath+"/servers/:id/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson, handler.RequireServerAccess(db)) app.POST(util.BasePath+"/servers/:id/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson, handler.RequireServerAccess(db))
app.GET(util.BasePath+"/servers/:id/settings", handler.GetServerSettings(db), handler.ValidSession, handler.RequireServerAccess(db)) app.GET(util.BasePath+"/servers/:id/settings", handler.GetServerSettings(db), handler.ValidSession, handler.RequireServerAccess(db))
app.POST(util.BasePath+"/servers/:id/settings", handler.SaveServerSettingsHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/servers/:id/settings", handler.SaveServerSettingsHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/servers/:id/interface", handler.UpdateServerInterfaceHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/servers/:id/keypair", handler.UpdateServerKeyPairHandler(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.POST(util.BasePath+"/backup/download", handler.DownloadBackup(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.POST(util.BasePath+"/backup/download", handler.DownloadBackup(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin)
app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
@@ -321,7 +320,7 @@ func initServerConfig(db store.IStore, tmplDir fs.FS) {
return return
} }
server, err := db.GetServer() server, err := db.GetServerByID(util.DefaultServerID)
if err != nil { if err != nil {
log.Fatalf("Cannot get server config: %v", err) log.Fatalf("Cannot get server config: %v", err)
} }
-6
View File
@@ -81,11 +81,6 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
log.Fatal(err) log.Fatal(err)
} }
tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html")
if err != nil {
log.Fatal(err)
}
tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html") tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@@ -129,7 +124,6 @@ func New(tmplDir fs.FS, extraData map[string]interface{}, secret [64]byte) *echo
templates["login.html"] = template.Must(template.New("login").Funcs(funcs).Parse(tmplLoginString)) templates["login.html"] = template.Must(template.New("login").Funcs(funcs).Parse(tmplLoginString))
templates["profile.html"] = template.Must(template.New("profile").Funcs(funcs).Parse(tmplBaseString + tmplProfileString)) templates["profile.html"] = template.Must(template.New("profile").Funcs(funcs).Parse(tmplBaseString + tmplProfileString))
templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString)) templates["clients.html"] = template.Must(template.New("clients").Funcs(funcs).Parse(tmplBaseString + tmplClientsString))
templates["server.html"] = template.Must(template.New("server").Funcs(funcs).Parse(tmplBaseString + tmplServerString))
templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString)) templates["global_settings.html"] = template.Must(template.New("global_settings").Funcs(funcs).Parse(tmplBaseString + tmplGlobalSettingsString))
templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString)) templates["users_settings.html"] = template.Must(template.New("users_settings").Funcs(funcs).Parse(tmplBaseString + tmplUsersSettingsString))
templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString)) templates["status.html"] = template.Must(template.New("status").Funcs(funcs).Parse(tmplBaseString + tmplStatusString))
+9 -48
View File
@@ -325,11 +325,13 @@ func (o *JsonDB) migrateLegacyServer() error {
} }
// NOTE: the legacy "server" directory is intentionally left in place // NOTE: the legacy "server" directory is intentionally left in place
// (not renamed/removed) at this stage. Existing store methods (GetServer, // (not renamed/removed). GetGlobalSettings/SaveGlobalSettings still read
// GetGlobalSettings, SaveServerInterface, ...) still read/write it // and write the app-wide settings record there (DNS/MTU/
// directly until they are switched over to the new per-server layout; // PersistentKeepalive are not yet per-server concerns); every other
// removing it now would break the running app. The rename-to-backup // server-scoped read/write now goes exclusively through the per-server
// step happens once those methods are migrated. // registry (servers/<id>.json via GetServerByID/UpdateServerInterface/
// UpdateServerKeyPair/GetServerSettings), so this directory is kept only
// as a landing spot for one-time migration and for global settings.
log.Printf("Migrated legacy single-server DB to multi-server layout (server id: %s); legacy db/server/ kept in place for now", serverID) log.Printf("Migrated legacy single-server DB to multi-server layout (server id: %s); legacy db/server/ kept in place for now", serverID)
return nil return nil
} }
@@ -387,27 +389,6 @@ func (o *JsonDB) GetGlobalSettings() (model.GlobalSetting, error) {
return settings, o.conn.Read("server", "global_settings", &settings) return settings, o.conn.Read("server", "global_settings", &settings)
} }
// GetServer func to query Server settings from the database
func (o *JsonDB) GetServer() (model.Server, error) {
server := model.Server{}
// read server interface information
serverInterface := model.ServerInterface{}
if err := o.conn.Read("server", "interfaces", &serverInterface); err != nil {
return server, err
}
// read server key pair information
serverKeyPair := model.ServerKeypair{}
if err := o.conn.Read("server", "keypair", &serverKeyPair); err != nil {
return server, err
}
// create Server object and return
server.Interface = &serverInterface
server.KeyPair = &serverKeyPair
return server, nil
}
func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) { func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
var clients []model.ClientData var clients []model.ClientData
@@ -429,7 +410,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
// generate client qrcode image in base64 // generate client qrcode image in base64
if hasQRCode && client.PrivateKey != "" { if hasQRCode && client.PrivateKey != "" {
server, _ := o.GetServer() server, _ := o.GetServerByID(util.DefaultServerID)
globalSettings, _ := o.GetGlobalSettings() globalSettings, _ := o.GetGlobalSettings()
png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256) png, err := qrcode.Encode(util.BuildClientConfig(client, server, globalSettings), qrcode.Medium, 256)
@@ -459,7 +440,7 @@ func (o *JsonDB) GetClientByID(clientID string, qrCodeSettings model.QRCodeSetti
// generate client qrcode image in base64 // generate client qrcode image in base64
if qrCodeSettings.Enabled && client.PrivateKey != "" { if qrCodeSettings.Enabled && client.PrivateKey != "" {
server, _ := o.GetServer() server, _ := o.GetServerByID(util.DefaultServerID)
globalSettings, _ := o.GetGlobalSettings() globalSettings, _ := o.GetGlobalSettings()
client := client client := client
if !qrCodeSettings.IncludeDNS { if !qrCodeSettings.IncludeDNS {
@@ -508,26 +489,6 @@ func (o *JsonDB) DeleteClient(clientID string) error {
return o.conn.Delete("clients", clientID) return o.conn.Delete("clients", clientID)
} }
func (o *JsonDB) SaveServerInterface(serverInterface model.ServerInterface) error {
serverInterfacePath := path.Join(path.Join(o.dbPath, "server"), "interfaces.json")
output := o.conn.Write("server", "interfaces", serverInterface)
err := util.ManagePerms(serverInterfacePath)
if err != nil {
return err
}
return output
}
func (o *JsonDB) SaveServerKeyPair(serverKeyPair model.ServerKeypair) error {
serverKeyPairPath := path.Join(path.Join(o.dbPath, "server"), "keypair.json")
output := o.conn.Write("server", "keypair", serverKeyPair)
err := util.ManagePerms(serverKeyPairPath)
if err != nil {
return err
}
return output
}
func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error { func (o *JsonDB) SaveGlobalSettings(globalSettings model.GlobalSetting) error {
globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json") globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json")
output := o.conn.Write("server", "global_settings", globalSettings) output := o.conn.Write("server", "global_settings", globalSettings)
-3
View File
@@ -11,13 +11,10 @@ type IStore interface {
SaveUser(user model.User) error SaveUser(user model.User) error
DeleteUser(username string) error DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error) GetGlobalSettings() (model.GlobalSetting, error)
GetServer() (model.Server, error)
GetClients(hasQRCode bool) ([]model.ClientData, error) GetClients(hasQRCode bool) ([]model.ClientData, error)
GetClientByID(clientID string, qrCode model.QRCodeSettings) (model.ClientData, error) GetClientByID(clientID string, qrCode model.QRCodeSettings) (model.ClientData, error)
SaveClient(client model.Client) error SaveClient(client model.Client) error
DeleteClient(clientID string) error DeleteClient(clientID string) error
SaveServerInterface(serverInterface model.ServerInterface) error
SaveServerKeyPair(serverKeyPair model.ServerKeypair) error
SaveGlobalSettings(globalSettings model.GlobalSetting) error SaveGlobalSettings(globalSettings model.GlobalSetting) error
GetWakeOnLanHosts() ([]model.WakeOnLanHost, error) GetWakeOnLanHosts() ([]model.WakeOnLanHost, error)
GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error) GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error)
-10
View File
@@ -128,16 +128,6 @@
</li> </li>
{{if .baseData.Admin}} {{if .baseData.Admin}}
<li class="nav-item">
<a href="{{.basePath}}/wg-server" class="nav-link {{if eq .baseData.Active "wg-server" }}active{{end}}">
<i class="nav-icon fas fa-server"></i>
<p>
Default Server Interface
</p>
</a>
</li>
<li class="nav-header">SETTINGS</li> <li class="nav-header">SETTINGS</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{.basePath}}/servers-settings" class="nav-link {{if eq .baseData.Active "servers" }}active{{end}}"> <a href="{{.basePath}}/servers-settings" class="nav-link {{if eq .baseData.Active "servers" }}active{{end}}">
-255
View File
@@ -1,255 +0,0 @@
{{define "title"}}
Default Server Interface
{{end}}
{{define "top_css"}}
{{end}}
{{define "username"}}
{{ .username }}
{{end}}
{{define "page_title"}}
Default Server Interface Settings
{{end}}
{{define "page_content"}}
<section class="content">
<div class="container-fluid">
<!-- <h5 class="mt-4 mb-2">Default Server Interface</h5> -->
<div class="row">
<!-- left column -->
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Interface</h3>
</div>
<!-- /.card-header -->
<!-- form start -->
<form role="form" id="frm_server_interface" name="frm_server_interface">
<div class="card-body">
<div class="form-group">
<label for="addresses" class="control-label">Server Interface Addresses</label>
<input type="text" data-role="tagsinput" class="form-control" id="addresses" value="">
</div>
<div class="form-group">
<label for="listen_port">Listen Port</label>
<input type="text" class="form-control" id="listen_port" name="listen_port"
placeholder="Listen Port" value="{{ .serverInterface.ListenPort }}">
</div>
<div class="form-group">
<label for="post_up">Post Up Script</label>
<input type="text" class="form-control" id="post_up" name="post_up"
placeholder="Post Up Script" value="{{ .serverInterface.PostUp }}">
</div>
<div class="form-group">
<label for="pre_down">Pre Down Script</label>
<input type="text" class="form-control" id="pre_down" name="pre_down"
placeholder="Pre Down Script" value="{{ .serverInterface.PreDown }}">
</div>
<div class="form-group">
<label for="post_down">Post Down Script</label>
<input type="text" class="form-control" id="post_down" name="post_down"
placeholder="Post Down Script" value="{{ .serverInterface.PostDown }}">
</div>
</div>
<!-- /.card-body -->
<div class="card-footer">
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
<!-- /.card -->
</div>
<!-- right column -->
<div class="col-md-6">
<div class="card card-danger">
<div class="card-header">
<h3 class="card-title">Key Pair</h3>
</div>
<!-- /.card-header -->
<!-- form start -->
<form role="form">
<div class="card-body">
<div class="form-group">
<label for="private_key">Private Key</label>
<div class="input-group input-group">
<input type="password" class="form-control" id="private_key" placeholder="Private Key"
value="{{ .serverKeyPair.PrivateKey }}" disabled>
<span class="input-group-append">
<button type="button" class="btn btn-danger btn-flat"
id="btn_show_private_key">Show</button>
</span>
</div>
</div>
<div class="form-group">
<label for="public_key">Public Key</label>
<input type="text" class="form-control" id="public_key" placeholder="Public Key"
value="{{ .serverKeyPair.PublicKey }}" disabled>
</div>
</div>
<!-- /.card-body -->
<div class="card-footer">
<button type="button" class="btn btn-danger" data-toggle="modal"
data-target="#modal_keypair_confirmation">Generate</button>
</div>
</form>
</div>
<!-- /.card -->
</div>
</div>
<!-- /.row -->
</div>
</section>
<div class="modal fade" id="modal_keypair_confirmation">
<div class="modal-dialog">
<div class="modal-content bg-warning">
<div class="modal-header">
<h4 class="modal-title">KeyPair Generation</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure to generate a new key pair for the Wireguard server?<br/>
The existing Client's peer public key need to be updated to keep the connection working.</p>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-outline-dark" id="btn_generate_confirm">Generate</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{{end}}
{{define "bottom_js"}}
<script>
function submitServerInterfaceSetting() {
const addresses = $("#addresses").val().split(",");
const listen_port = $("#listen_port").val();
const post_up = $("#post_up").val();
const pre_down = $("#pre_down").val();
const post_down = $("#post_down").val();
const data = {"addresses": addresses, "listen_port": listen_port, "post_up": post_up, "pre_down": pre_down, "post_down": post_down};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/wg-server/interfaces',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function(data) {
$("#modal_new_client").modal('hide');
toastr.success('Updated Wireguard server interface addresses successfully');
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
</script>
<script>
// Wireguard Interface Addresses tag input
$("#addresses").tagsInput({
'width': '100%',
// 'height': '75%',
'interactive': true,
'defaultText': 'Add More',
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
// Load server addresses to the form
{{range .serverInterface.Addresses}}
$("#addresses").removeTag('{{.}}');
$("#addresses").addTag('{{.}}');
{{end}}
// Wireguard Interface Addresses form validation
$(document).ready(function () {
$.validator.setDefaults({
submitHandler: function () {
submitServerInterfaceSetting();
}
});
$("#frm_server_interface").validate({
rules: {
listen_port: {
required: true,
digits: true,
range: [1, 65535]
}
},
messages: {
listen_port: {
required: "Please enter a port",
digits: "Port must be an integer",
range: "Port must be in range 1..65535"
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
}
});
});
// Wireguard Key Pair generation confirmation button
$(document).ready(function () {
$("#btn_generate_confirm").click(function () {
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/wg-server/keypair',
dataType: 'json',
contentType: "application/json",
success: function(data) {
$("#modal_keypair_confirmation").modal('hide');
toastr.success('Generate new key pair successfully');
// update the UI
$("#private_key").val(data['private_key']);
$("#public_key").val(data['public_key']);
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
// Show private key button event
$(document).ready(function () {
$("#btn_show_private_key").click(function () {
const privateElement = document.getElementById("private_key");
const btnElement = document.getElementById("btn_show_private_key");
if (privateElement.type === 'password') {
privateElement.type = 'text';
btnElement.innerText = 'Hide';
} else {
privateElement.type = 'password';
btnElement.innerText = 'Show';
}
});
});
</script>
{{end}}
+177
View File
@@ -114,12 +114,73 @@ All Servers
<!-- /.modal-dialog --> <!-- /.modal-dialog -->
</div> </div>
<!-- /.modal --> <!-- /.modal -->
<div class="modal fade" id="modal_server_interface">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Server Interface</h4>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form name="frm_server_interface" id="frm_server_interface">
<div class="modal-body">
<input type="hidden" id="_iface_server_id" name="_iface_server_id">
<div class="form-group">
<label for="_iface_addresses" class="control-label">Server Interface Addresses</label>
<input type="text" class="form-control" id="_iface_addresses"
placeholder="e.g. 10.20.22.1/24, fd00::1/64">
<small class="form-text text-muted">Comma-separated list of address ranges.</small>
</div>
<div class="form-group">
<label for="_iface_listen_port" class="control-label">Listen Port</label>
<input type="text" class="form-control" id="_iface_listen_port" name="_iface_listen_port"
placeholder="e.g. 51822">
</div>
<div class="form-group">
<label for="_iface_post_up" class="control-label">Post Up Script</label>
<input type="text" class="form-control" id="_iface_post_up" placeholder="Post Up Script">
</div>
<div class="form-group">
<label for="_iface_pre_down" class="control-label">Pre Down Script</label>
<input type="text" class="form-control" id="_iface_pre_down" placeholder="Pre Down Script">
</div>
<div class="form-group">
<label for="_iface_post_down" class="control-label">Post Down Script</label>
<input type="text" class="form-control" id="_iface_post_down" placeholder="Post Down Script">
</div>
<hr>
<div class="form-group">
<label class="control-label">Public Key</label>
<input type="text" class="form-control" id="_iface_public_key" disabled>
</div>
<button type="button" class="btn btn-outline-danger btn-sm" id="btn_regenerate_keypair">
Regenerate Keypair
</button>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Save</button>
</div>
</form>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{{end}} {{end}}
{{define "bottom_js"}} {{define "bottom_js"}}
<script> <script>
// cache of the last-fetched server list, keyed by id, so the Interface
// modal can prefill fields without a second round trip
var serversById = {};
function renderServersList(data) { function renderServersList(data) {
$.each(data, function (index, obj) { $.each(data, function (index, obj) {
serversById[obj.id] = obj;
const addresses = (obj.Interface && obj.Interface.addresses) ? obj.Interface.addresses.join(", ") : ""; const addresses = (obj.Interface && obj.Interface.addresses) ? obj.Interface.addresses.join(", ") : "";
const listenPort = obj.Interface ? obj.Interface.listen_port : ""; const listenPort = obj.Interface ? obj.Interface.listen_port : "";
const interfaceName = obj.Interface ? obj.Interface.name : ""; const interfaceName = obj.Interface ? obj.Interface.name : "";
@@ -138,6 +199,10 @@ All Servers
<button type="button" class="btn btn-outline-secondary btn-sm" data-toggle="modal" <button type="button" class="btn btn-outline-secondary btn-sm" data-toggle="modal"
data-target="#modal_server_settings" data-serverid="${obj.id}">Settings</button> data-target="#modal_server_settings" data-serverid="${obj.id}">Settings</button>
</div> </div>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary btn-sm" data-toggle="modal"
data-target="#modal_server_interface" data-serverid="${obj.id}">Interface</button>
</div>
<hr> <hr>
<span class="info-box-text"><i class="fas fa-server"></i> ${safeName}</span> <span class="info-box-text"><i class="fas fa-server"></i> ${safeName}</span>
<span class="info-box-text"><i class="fas fa-fingerprint"></i> ID: ${obj.id}</span> <span class="info-box-text"><i class="fas fa-fingerprint"></i> ID: ${obj.id}</span>
@@ -293,6 +358,118 @@ All Servers
}); });
}); });
// Server interface modal event: prefill from the cached server list data
$("#modal_server_interface").on('show.bs.modal', function (event) {
const button = $(event.relatedTarget);
const serverId = button.data('serverid');
const modal = $(this);
const obj = serversById[serverId] || {};
modal.find("#_iface_server_id").val(serverId);
modal.find("#_iface_addresses").val((obj.Interface && obj.Interface.addresses) ? obj.Interface.addresses.join(", ") : "");
modal.find("#_iface_listen_port").val(obj.Interface ? obj.Interface.listen_port : "");
modal.find("#_iface_post_up").val(obj.Interface ? obj.Interface.post_up : "");
modal.find("#_iface_pre_down").val(obj.Interface ? obj.Interface.pre_down : "");
modal.find("#_iface_post_down").val(obj.Interface ? obj.Interface.post_down : "");
modal.find("#_iface_public_key").val(obj.KeyPair ? obj.KeyPair.public_key : "");
});
function submitServerInterface() {
const serverId = $("#_iface_server_id").val();
const addresses = $("#_iface_addresses").val().split(",").map(function (a) {
return a.trim();
}).filter(function (a) {
return a !== "";
});
const data = {
"addresses": addresses,
"listen_port": $("#_iface_listen_port").val(),
"post_up": $("#_iface_post_up").val(),
"pre_down": $("#_iface_pre_down").val(),
"post_down": $("#_iface_post_down").val()
};
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/servers/' + serverId + '/interface',
dataType: 'json',
contentType: "application/json",
data: JSON.stringify(data),
success: function (data) {
$("#modal_server_interface").modal('hide');
toastr.success("Updated server interface successfully");
$('#servers-list').empty();
populateServersList();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
}
$(document).ready(function () {
$("#frm_server_interface").validate({
rules: {
_iface_listen_port: {
required: true,
digits: true,
range: [1, 65535]
}
},
messages: {
_iface_listen_port: {
required: "Please enter a port",
digits: "Port must be an integer",
range: "Port must be in range 1..65535"
}
},
errorElement: 'span',
errorPlacement: function (error, element) {
error.addClass('invalid-feedback');
element.closest('.form-group').append(error);
},
highlight: function (element, errorClass, validClass) {
$(element).addClass('is-invalid');
},
unhighlight: function (element, errorClass, validClass) {
$(element).removeClass('is-invalid');
},
submitHandler: function (form) {
submitServerInterface();
}
});
});
// Regenerate keypair button: confirm, then POST and refresh the field
$(document).ready(function () {
$("#btn_regenerate_keypair").click(function () {
const serverId = $("#_iface_server_id").val();
if (!confirm("Are you sure you want to generate a new key pair for this server?\n" +
"All existing peers will lose connectivity until their configuration is updated with the new public key.")) {
return;
}
$.ajax({
cache: false,
method: 'POST',
url: '{{.basePath}}/servers/' + serverId + '/keypair',
dataType: 'json',
contentType: "application/json",
success: function (data) {
toastr.success("Generated new key pair successfully");
$("#_iface_public_key").val(data['public_key']);
$('#servers-list').empty();
populateServersList();
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
toastr.error(responseJson['message']);
}
});
});
});
$(document).ready(function () { $(document).ready(function () {
$.validator.setDefaults({ $.validator.setDefaults({
submitHandler: function (form) { submitHandler: function (form) {
+2 -2
View File
@@ -491,7 +491,7 @@ func ValidateAndFixSubnetRanges(db store.IStore) error {
return nil return nil
} }
server, err := db.GetServer() server, err := db.GetServerByID(DefaultServerID)
if err != nil { if err != nil {
return err return err
} }
@@ -639,7 +639,7 @@ func SendRequestedConfigsToTelegram(db store.IStore, userid int64) []string {
} }
// build config // build config
server, _ := db.GetServer() server, _ := db.GetServerByID(DefaultServerID)
globalSettings, _ := db.GetGlobalSettings() globalSettings, _ := db.GetGlobalSettings()
config := BuildClientConfig(*clientData.Client, server, globalSettings) config := BuildClientConfig(*clientData.Client, server, globalSettings)
configData := []byte(config) configData := []byte(config)