Add server creation UI + user-server access assignment (step 3)

New admin-only page at /servers-settings (templates/servers.html) lists
servers and creates new ones via POST /servers (ID/name/interface/
addresses/port, key pair generated server-side). Nav gets a "Servers"
link.

templates/users_settings.html gains a multi-select "Server Access"
field wired to the server_ids support added to create-user/update-user
in the previous commit, so admins can now actually assign non-admin
users to specific servers through the UI.
This commit is contained in:
sysops
2026-07-11 23:24:05 +02:00
parent 5b72a7c118
commit 3842c7a534
6 changed files with 396 additions and 1 deletions
+107
View File
@@ -199,6 +199,15 @@ func UsersSettings() echo.HandlerFunc {
}
}
// ServersPage renders the server list/create page
func ServersPage() echo.HandlerFunc {
return func(c echo.Context) error {
return c.Render(http.StatusOK, "servers.html", map[string]interface{}{
"baseData": model.BaseData{Active: "servers", CurrentUser: currentUser(c), Admin: isAdmin(c)},
})
}
}
// UpdateUser to update user information
func UpdateUser(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
@@ -256,6 +265,19 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
user.Admin = admin
}
// only an admin may change which servers a user can access
if isAdmin(c) {
if rawIDs, ok := data["server_ids"].([]interface{}); ok {
serverIDs := make([]string, 0, len(rawIDs))
for _, v := range rawIDs {
if id, ok := v.(string); ok && util.ValidateRecordID(id) {
serverIDs = append(serverIDs, id)
}
}
user.ServerIDs = serverIDs
}
}
if err := db.DeleteUser(previousUsername); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
@@ -308,6 +330,14 @@ func CreateUser(db store.IStore) echo.HandlerFunc {
user.Admin = admin
if rawIDs, ok := data["server_ids"].([]interface{}); ok {
for _, v := range rawIDs {
if id, ok := v.(string); ok && util.ValidateRecordID(id) {
user.ServerIDs = append(user.ServerIDs, id)
}
}
}
if err := db.SaveUser(user); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()})
}
@@ -476,6 +506,83 @@ func GetServerClients(db store.IStore) echo.HandlerFunc {
}
}
// CreateServer handler creates a new WireGuard server (step 3 of the
// multi-server extension). Admin-only. Generates a fresh key pair,
// validates the ID/interface name/subnet, and stores the server plus its
// per-server settings.
func CreateServer(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
type createServerRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Interface string `json:"interface"`
Addresses []string `json:"addresses"`
ListenPort int `json:"listen_port"`
}
var req createServerRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Bad post data"})
}
if !util.ValidateRecordID(req.ID) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid server ID (letters, digits, - and _ only)"})
}
if !util.ValidateInterfaceName(req.Interface) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid interface name (max 15 chars, letters/digits/-/_ only)"})
}
if !util.ValidateServerAddresses(req.Addresses) {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Address ranges must be in CIDR format"})
}
if req.ListenPort <= 0 || req.ListenPort > 65535 {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid listen port"})
}
if req.Name == "" {
req.Name = req.ID
}
if _, err := db.GetServerByID(req.ID); err == nil {
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "A server with this ID already exists"})
}
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
log.Error("Cannot generate wireguard key pair: ", err)
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot generate Wireguard key pair"})
}
server := model.Server{
ID: req.ID,
Name: req.Name,
KeyPair: &model.ServerKeypair{
PrivateKey: key.String(),
PublicKey: key.PublicKey().String(),
UpdatedAt: time.Now().UTC(),
},
Interface: &model.ServerInterface{
Name: req.Interface,
Addresses: req.Addresses,
ListenPort: req.ListenPort,
UpdatedAt: time.Now().UTC(),
},
}
if err := db.CreateServer(server); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Cannot create server: %v", err)})
}
settings := model.ServerSetting{
ConfigFilePath: fmt.Sprintf("/etc/wireguard/%s.conf", req.Interface),
UpdatedAt: time.Now().UTC(),
}
if err := db.SaveServerSettings(req.ID, settings); err != nil {
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, fmt.Sprintf("Server created but settings failed: %v", err)})
}
log.Infof("Created server %s (%s)", req.ID, req.Name)
return c.JSON(http.StatusOK, server)
}
}
// NewClient handler
func NewClient(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {