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 ++++++++++++++++++++++++++++++++++++++++++
---
## 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
// param for /servers/:id/... routes, or util.DefaultServerID for the
// legacy bare routes (/, /new-client, /wg-server, ...) which always
// operate on the migrated default server.
// legacy bare routes (/, /new-client, ...) which always operate on the
// migrated default server.
func resolveServerID(c echo.Context) string {
if id := c.Param("id"); id != "" {
return id
@@ -1168,27 +1168,22 @@ func RemoveClient(db store.IStore) echo.HandlerFunc {
}
}
// WireGuardServer handler
func WireGuardServer(db store.IStore) echo.HandlerFunc {
// UpdateServerInterfaceHandler updates the WireGuard interface settings
// (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 {
server, err := db.GetServer()
if err != nil {
log.Error("Cannot get server config: ", err)
serverID := c.Param("id")
if _, err := db.GetServerByID(serverID); err != nil {
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
c.Bind(&serverInterface)
if err := c.Bind(&serverInterface); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
// validate the input addresses
if util.ValidateServerAddresses(serverInterface.Addresses) == false {
@@ -1198,31 +1193,27 @@ func WireGuardServerInterfaces(db store.IStore) echo.HandlerFunc {
serverInterface.UpdatedAt = time.Now().UTC()
// write config to the database (legacy single-server collection)
if err := db.SaveServerInterface(serverInterface); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Interface IP address must be in CIDR format"})
if err := db.UpdateServerInterface(serverID, serverInterface); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot update server interface: %v", err)})
}
// keep the new per-server registry record for the default server in
// 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)
log.Infof("Updated wireguard server interface settings for server %s: %v", serverID, serverInterface)
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated interface addresses successfully"})
}
}
// WireGuardServerKeyPair handler to generate private and public keys
func WireGuardServerKeyPair(db store.IStore) echo.HandlerFunc {
// UpdateServerKeyPairHandler generates a fresh WireGuard key pair for a
// 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 {
// 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()
if err != nil {
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.UpdatedAt = time.Now().UTC()
if err := db.SaveServerKeyPair(serverKeyPair); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"})
if err := db.UpdateServerKeyPair(serverID, serverKeyPair); err != nil {
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
// 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)
log.Infof("Updated wireguard server key pair for server %s", serverID)
return c.JSON(http.StatusOK, serverKeyPair)
}
@@ -1379,26 +1364,14 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc {
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 {
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)
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
func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
server, err := db.GetServer()
server, err := db.GetServerByID(util.DefaultServerID)
if err != nil {
log.Error("Cannot fetch server config from database: ", err)
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 {
serverID := resolveServerID(c)
var server model.Server
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)
}
server, err := db.GetServerByID(serverID)
if err != nil {
log.Error("Cannot get server config: ", err)
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+"/remove-client", handler.RemoveClient(db), handler.ValidSession, handler.ContentTypeJson)
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.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)
@@ -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.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/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.GET(util.BasePath+"/api/clients", handler.GetClients(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
}
server, err := db.GetServer()
server, err := db.GetServerByID(util.DefaultServerID)
if err != nil {
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)
}
tmplServerString, err := util.StringFromEmbedFile(tmplDir, "server.html")
if err != nil {
log.Fatal(err)
}
tmplGlobalSettingsString, err := util.StringFromEmbedFile(tmplDir, "global_settings.html")
if err != nil {
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["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["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["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))
+9 -48
View File
@@ -325,11 +325,13 @@ func (o *JsonDB) migrateLegacyServer() error {
}
// NOTE: the legacy "server" directory is intentionally left in place
// (not renamed/removed) at this stage. Existing store methods (GetServer,
// GetGlobalSettings, SaveServerInterface, ...) still read/write it
// directly until they are switched over to the new per-server layout;
// removing it now would break the running app. The rename-to-backup
// step happens once those methods are migrated.
// (not renamed/removed). GetGlobalSettings/SaveGlobalSettings still read
// and write the app-wide settings record there (DNS/MTU/
// PersistentKeepalive are not yet per-server concerns); every other
// server-scoped read/write now goes exclusively through the per-server
// 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)
return nil
}
@@ -387,27 +389,6 @@ func (o *JsonDB) GetGlobalSettings() (model.GlobalSetting, error) {
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) {
var clients []model.ClientData
@@ -429,7 +410,7 @@ func (o *JsonDB) GetClients(hasQRCode bool) ([]model.ClientData, error) {
// generate client qrcode image in base64
if hasQRCode && client.PrivateKey != "" {
server, _ := o.GetServer()
server, _ := o.GetServerByID(util.DefaultServerID)
globalSettings, _ := o.GetGlobalSettings()
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
if qrCodeSettings.Enabled && client.PrivateKey != "" {
server, _ := o.GetServer()
server, _ := o.GetServerByID(util.DefaultServerID)
globalSettings, _ := o.GetGlobalSettings()
client := client
if !qrCodeSettings.IncludeDNS {
@@ -508,26 +489,6 @@ func (o *JsonDB) DeleteClient(clientID string) error {
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 {
globalSettingsPath := path.Join(path.Join(o.dbPath, "server"), "global_settings.json")
output := o.conn.Write("server", "global_settings", globalSettings)
-3
View File
@@ -11,13 +11,10 @@ type IStore interface {
SaveUser(user model.User) error
DeleteUser(username string) error
GetGlobalSettings() (model.GlobalSetting, error)
GetServer() (model.Server, error)
GetClients(hasQRCode bool) ([]model.ClientData, error)
GetClientByID(clientID string, qrCode model.QRCodeSettings) (model.ClientData, error)
SaveClient(client model.Client) error
DeleteClient(clientID string) error
SaveServerInterface(serverInterface model.ServerInterface) error
SaveServerKeyPair(serverKeyPair model.ServerKeypair) error
SaveGlobalSettings(globalSettings model.GlobalSetting) error
GetWakeOnLanHosts() ([]model.WakeOnLanHost, error)
GetWakeOnLanHost(macAddress string) (*model.WakeOnLanHost, error)
-10
View File
@@ -128,16 +128,6 @@
</li>
{{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-item">
<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 -->
</div>
<!-- /.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}}
{{define "bottom_js"}}
<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) {
$.each(data, function (index, obj) {
serversById[obj.id] = obj;
const addresses = (obj.Interface && obj.Interface.addresses) ? obj.Interface.addresses.join(", ") : "";
const listenPort = obj.Interface ? obj.Interface.listen_port : "";
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"
data-target="#modal_server_settings" data-serverid="${obj.id}">Settings</button>
</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>
<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>
@@ -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 () {
$.validator.setDefaults({
submitHandler: function (form) {
+2 -2
View File
@@ -491,7 +491,7 @@ func ValidateAndFixSubnetRanges(db store.IStore) error {
return nil
}
server, err := db.GetServer()
server, err := db.GetServerByID(DefaultServerID)
if err != nil {
return err
}
@@ -639,7 +639,7 @@ func SendRequestedConfigsToTelegram(db store.IStore, userid int64) []string {
}
// build config
server, _ := db.GetServer()
server, _ := db.GetServerByID(DefaultServerID)
globalSettings, _ := db.GetGlobalSettings()
config := BuildClientConfig(*clientData.Client, server, globalSettings)
configData := []byte(config)